Джанго: Принудительный выбор связан?

Я создал модель и визуализирую для нее стандартную/немодифицированную форму модели. Это само по себе генерирует 64 SQL-запроса, потому что у него довольно много внешних ключей, а те, в свою очередь, имеют больше внешних ключей.

Можно ли заставить его всегда (по умолчанию) выполнять select_related каждый раз, когда возвращается одна из этих моделей?


person mpen    schedule 03.02.2011    source источник


Ответы (3)


Вы можете создать собственный менеджер и просто переопределить get_queryset, чтобы он применялся везде. Например:

class MyManager(models.Manager):
    def get_queryset(self):
        return super(MyManager, self).get_queryset().select_related('foo', 'bar')

(До Django 1.6 это было get_query_set).

person M Somerville    schedule 05.07.2012
comment
Как это сделать при использовании models.QuerySet? - person François Constant; 25.08.2015
comment
Я думаю, вам нужно предоставить больше информации или какой-то пример кода, что вы имеете в виду; если у вас есть QuerySet, вы можете напрямую вызвать select_related для него. - person M Somerville; 25.08.2015
comment
as_manager возвращает начальный объект менеджера; документация на docs.djangoproject.com/en/1.8/ tops/db/managers/#from-queryset объясняет, как использовать собственный менеджер вместе с пользовательским набором запросов. Вы должны иметь возможность определить своего менеджера, как указано выше, определить свой собственный набор запросов, а затем использовать что-то вроде objects = MyManager.from_queryset(MyQueryset)() - person M Somerville; 25.08.2015

Вот еще забавный трюк:

class DefaultSelectOrPrefetchManager(models.Manager):
    def __init__(self, *args, **kwargs):
        self._select_related = kwargs.pop('select_related', None)
        self._prefetch_related = kwargs.pop('prefetch_related', None)

        super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)

    def get_queryset(self, *args, **kwargs):
        qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)

        if self._select_related:
            qs = qs.select_related(*self._select_related)
        if self._prefetch_related:
            qs = qs.prefetch_related(*self._prefetch_related)

        return qs


class Sandwich(models.Model):
    bread = models.ForeignKey(Bread)
    extras = models.ManyToManyField(Extra)

    # ...

    objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',))

Затем вы можете легко повторно использовать менеджер между классами моделей. В качестве примера использования это было бы уместно, если бы у вас был метод __unicode__ в модели, который отображал строку, содержащую некоторую информацию из связанной модели (или что-то еще, что означало, что связанная модель почти всегда требуется).

...и если вы действительно хотите пошалить, вот более общая версия. Это позволяет вам вызывать любую последовательность методов в наборе запросов по умолчанию с любой комбинацией args или kwargs. В коде могут быть некоторые ошибки, но вы поняли идею.

from django.db import models


class MethodCalls(object):
    """
    A mock object which logs chained method calls.
    """
    def __init__(self):
        self._calls = []

    def __getattr__(self, name):
        c = Call(self, name)
        self._calls.append(c)
        return c

    def __iter__(self):
        for c in self._calls:
            yield tuple(c)


class Call(object):
    """
    Used by `MethodCalls` objects internally to represent chained method calls.
    """
    def __init__(self, calls_obj, method_name):
        self._calls = calls_obj
        self.method_name = method_name

    def __call__(self, *method_args, **method_kwargs):
        self.method_args = method_args
        self.method_kwargs = method_kwargs

        return self._calls

    def __iter__(self):
        yield self.method_name
        yield self.method_args
        yield self.method_kwargs


class DefaultQuerysetMethodCallsManager(models.Manager):
    """
    A model manager class which allows specification of a sequence of
    method calls to be applied by default to base querysets.
    `DefaultQuerysetMethodCallsManager` instances expose a property
    `default_queryset_method_calls` to which chained method calls can be
    applied to indicate which methods should be called on base querysets.
    """
    def __init__(self, *args, **kwargs):
        self.default_queryset_method_calls = MethodCalls()

        super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs)

    def get_queryset(self, *args, **kwargs):
        qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs)

        for method_name, method_args, method_kwargs in self.default_queryset_method_calls:
            qs = getattr(qs, method_name)(*method_args, **method_kwargs)

        return qs


class Sandwich(models.Model):
    bread = models.ForeignKey(Bread)
    extras = models.ManyToManyField(Extra)

    # Other field definitions...

    objects = DefaultQuerysetMethodCallsManager()
    objects.default_queryset_method_calls.filter(
        bread__type='wheat',
    ).select_related(
        'bread',
    ).prefetch_related(
        'extras',
    )

Объект MethodCalls, вдохновленный макетом Python, — это попытка сделать API более естественным. Некоторым это может показаться немного запутанным. Если это так, вы можете заменить этот код на __init__ arg или kwarg, который просто принимает кортеж информации о вызове метода.

person David Sanders    schedule 22.01.2014
comment
Это действительно спасло мой день — вместо того, чтобы писать тонны ModelForm, переопределяя ModelChoiceField, пока коровы не вернутся домой. (Особенно, если MyModel.__unicode__() использует то, что должно быть select_related. - person Tomasz Gandor; 13.05.2014
comment
Отличный ответ. Это решает большинство моих проблем, связанных с n+1. - person Eldamir; 25.08.2015
comment
Примечание: get_query_set был устарело в Django 1.6. Теперь его следует заменить на get_queryset. - person keithb; 24.09.2016

Создайте собственный models.Manager и переопределите все методы (filter, get и т. д.). .) и добавляйте select_related к каждому запросу. Затем установите этот менеджер как атрибут objects модели.

Я бы порекомендовал просто просмотреть ваш код и добавить select_related там, где это необходимо, потому что выполнение select_related для всего может вызвать серьезные проблемы с производительностью в будущем (и было бы не совсем ясно, откуда это берется).

person Sam Dolan    schedule 03.02.2011
comment
Затем мне нужно переопределить все поля формы в форме модели, чтобы я мог установить набор запросов вручную... - person mpen; 04.02.2011
comment
Ну, вы всегда можете согласиться с моим первым предложением. Я просто предупреждал вас о возможных пагубных проблемах с производительностью, с которыми вы можете столкнуться в будущем. Есть способы сделать это только для ModelForm без переопределения всего, но ответ действительно будет зависеть от того, что именно вам нужно сделать. Если вам нужна помощь, просто создайте еще один вопрос с более подробной информацией. - person Sam Dolan; 04.02.2011
comment
Что ж, у меня есть модель Address со ссылками на почтовый индекс, город, провинцию и страну. Он почти никогда не будет отображаться без этих полей, поэтому я решил, что могу включить его по умолчанию. - person mpen; 04.02.2011
comment
Просто добавьте пользовательский менеджер, который я предложил для этой модели Address, и все будет готово. - person Sam Dolan; 04.02.2011
comment
@ Izzad-DinRuhulessin Прошли годы, но я думаю, что использовал их для поиска города / провинции / страны - они не были частью более крупного адреса. - person mpen; 15.08.2014