Django ORM: оптимизация запросов, включающих отношения многие ко многим - PullRequest
5 голосов
/ 29 октября 2009

У меня следующая структура модели:

class Container(models.Model):
    pass

class Generic(models.Model):
    name = models.CharacterField(unique=True)
    cont = models.ManyToManyField(Container, null=True)
    # It is possible to have a Generic object not associated with any container, 
    # thats why null=True

class Specific1(Generic):
    ...

class Specific2(Generic):
    ...

...

class SpecificN(Generic):
    ...

Скажем, мне нужно получить все модели Specific, которые связаны с конкретным контейнером.

SQL для этого более или менее тривиален, но это не вопрос. К сожалению, я не очень опытен в работе с ORM (в частности, с ORM в Django), поэтому я могу пропустить шаблон здесь.

Когда сделано методом грубой силы, -

c = Container.objects.get(name='somename') # this gets me the container
items = c.generic_set.all() 
# this gets me all Generic objects, that are related to the container
# Now what? I need to get to the actual Specific objects, so I need to somehow
# get the type of the underlying Specific object and get it
for item in items:
    spec = getattr(item, item.get_my_specific_type())

это приводит к тонне попаданий в дБ (по одному для каждой общей записи, относящейся к контейнеру), так что это явно не тот способ, которым это можно сделать. Теперь, возможно, это можно сделать путем непосредственного получения объектов SpecificX:

s = Specific1.objects.filter(cont__name='somename')
# This gets me all Specific1 objects for the specified container
...
# do it for every Specific type

таким образом, БД будет поражен один раз для каждого конкретного типа (я думаю, это приемлемо).

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

Повторюсь, конечный результат должен быть коллекцией объектов SpecificX (не Generic).

Ответы [ 3 ]

2 голосов
/ 29 октября 2009

Я думаю, вы уже обрисовали две простые возможности. Либо вы делаете один запрос фильтра к универсальному, а затем приводите каждый элемент к его определенному подтипу (в результате получается n + 1 запросов, где n - количество возвращаемых элементов), либо вы делаете отдельный запрос для каждой конкретной таблицы (приводит к k запросов, где k - количество конкретных типов).

На самом деле стоит проверить, какой из них быстрее в реальности. Второй кажется лучше, потому что он (вероятно) меньше запросов, но каждый из этих запросов должен выполнить соединение с промежуточной таблицей m2m. В первом случае вы делаете только один запрос на присоединение, а затем много простых. Некоторые базы данных работают лучше с большим количеством небольших запросов, чем с меньшим количеством более сложных запросов.

Если второе на самом деле значительно быстрее для вашего варианта использования, и вы готовы проделать некоторую дополнительную работу по очистке вашего кода, должна быть возможность написать собственный метод менеджера для универсальной модели, который «предварительно выбирает» «все данные подтипа из соответствующих специфических таблиц для данного набора запросов, используя только один запрос на таблицу подтипа; аналогично тому, как этот фрагмент оптимизирует общие внешние ключи с помощью массовой предварительной выборки. Это даст вам те же запросы, что и во втором варианте, с синтаксисом DRYer для первого варианта.

1 голос
/ 29 октября 2009

Не полный ответ, но вы можете избежать большого количества хитов, делая это

items= list(items)
for item in items:
    spec = getattr(item, item.get_my_specific_type())

вместо этого:

for item in items:
    spec = getattr(item, item.get_my_specific_type())

Действительно, путем принудительного приведения к списку Python вы заставляете django orm загружать все элементы в вашем наборе запросов. Затем он делает это в одном запросе.

0 голосов
/ 10 ноября 2009

Я случайно наткнулся на следующий пост, который в значительной степени отвечает на ваш вопрос:

http://lazypython.blogspot.com/2008/11/timeline-view-in-django.html

...