Джанго: Сила выбора связана? - PullRequest
35 голосов
/ 04 февраля 2011

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

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

Ответы [ 3 ]

45 голосов
/ 06 июля 2012

Вы можете создать собственный менеджер и просто переопределить 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).

36 голосов
/ 22 января 2014

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

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__, который отображал строку, включающую некоторую информацию из связанной модели (или что-либо еще, что означало, что связанная модель была почти всегда * 1006). * обязательно).

... и если вы действительно хотите стать дурацким, вот более обобщенная версия. Это позволяет вам вызывать любую последовательность методов в наборе запросов по умолчанию с любой комбинацией 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-mock - это попытка сделать API более естественным. Некоторые могут найти это немного запутанным. Если это так, вы можете передать этот код для __init__ arg или kwarg, который просто принимает кортеж информации о вызове метода.

2 голосов
/ 04 февраля 2011

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

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

...