django: предварительная выборка связанных объектов GenericForeignKey
Предположим, у меня есть модель Box
с GenericForeignKey
это указывает либо на Apple
экземпляр или Chocolate
пример. Apple
а также Chocolate
, в свою очередь, есть ForeignKeys для Farm
а также Factory
соответственно. Я хочу отобразить список Box
ES, для которого мне нужен доступ Farm
а также Factory
, Как мне сделать это в минимально возможном количестве запросов к БД?
Минимальный иллюстративный пример:
class Farm(Model):
...
class Apple(Model):
farm = ForeignKey(Farm)
...
class Factory(Model):
...
class Chocolate(Model):
factory = ForeignKey(Factory)
...
class Box(Model)
content_type = ForeignKey(ContentType)
object_id = PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
...
def __unicode__(self):
if self.content_type == ContentType.objects.get_for_model(Apple):
apple = self.content_object
return "Apple {} from Farm {}".format(apple, apple.farm)
elif self.content_type == ContentType.objects.get_for_model(Chocolate):
chocolate = self.content_object
return "Chocolate {} from Factory {}".format(chocolate, chocolate.factory)
Вот несколько вещей, которые я попробовал. Во всех этих примерах N - количество ящиков. Количество запросов предполагает, что ContentType
с для Apple
а также Chocolate
уже были кэшированы, поэтому get_for_model()
звонки не попадают в БД.
1) Наивный:
print [box for box in Box.objects.all()]
При этом выполняется 1 (выбор коробок) + N (выбор яблок или шоколада для каждой коробки) + N (выбор фермы для каждого яблока и фабрика для каждого шоколада).
2) select_related
здесь не помогает, потому что Box.content_object
это GenericForeignKey
,
3) Начиная с версии 1.4, prefetch_related
может принести GenericForeignKey
s.
print [box for box in Box.objects.prefetch_related('content_object').all()]
Это делает 1 (выбор коробок) + 2 (выбор яблок и конфет для всех коробок) + N (выбор фермы для каждого яблока и фабрики для каждого шоколада).
4) Видимо prefetch_related
недостаточно умен, чтобы следовать за ForeignKeys из GenericForeignKeys. Если я попробую:
print [box for box in Box.objects.prefetch_related(
'content_object__farm',
'content_object__factory').all()]
справедливо жалуется, что Chocolate
объекты не имеют farm
поле, и наоборот.
5) я мог бы сделать:
apple_ctype = ContentType.objects.get_for_model(Apple)
chocolate_ctype = ContentType.objects.get_for_model(Chocolate)
boxes_with_apples = Box.objects.filter(content_type=apple_ctype).prefetch_related('content_object__farm')
boxes_with_chocolates = Box.objects.filter(content_type=chocolate_ctype).prefetch_related('content_object__factory')
При этом выполняется 1 (выбор коробок) + 2 (выбор яблок и шоколада для всех коробок) + 2 (выбор фермы для всех яблок и фабрики для всех конфет). Недостатком является то, что мне нужно объединить и отсортировать два набора запросов (boxes_with_apples
, boxes_with_chocolates
) вручную. В моем реальном приложении я отображаю эти поля в разбитом на страницы ModelAdmin. Не очевидно, как интегрировать это решение там. Может быть, я мог бы написать собственный Paginator, чтобы сделать это кеширование прозрачным?
6) Я мог бы собрать воедино что-то основанное на этом, что также делает O(1) запросов. Но я бы лучше не связывался с внутренностями (_content_object_cache
) если я могу избежать этого.
В итоге: для печати коробки требуется доступ к ForeignKeys из GenericForeignKey. Как я могу напечатать N коробок в O(1) запросах? (5) лучшее, что я могу сделать, или есть более простое решение?
Бонусные баллы: Как бы вы реорганизовали эту схему БД, чтобы упростить такие запросы?
1 ответ
Вы можете вручную реализовать что-то вроде prefetch_selected
и использовать Джанго select_related
метод, который сделает соединение в запросе к базе данных.
apple_ctype = ContentType.objects.get_for_model(Apple)
chocolate_ctype = ContentType.objects.get_for_model(Chocolate)
boxes = Box.objects.all()
content_objects = {}
# apples
content_objects[apple_ctype.id] = Apple.objects.select_related(
'farm').in_bulk(
[b.object_id for b in boxes if b.content_type == apple_ctype]
)
# chocolates
content_objects[chocolate_ctype.id] = Chocolate.objects.select_related(
'factory').in_bulk(
[b.object_id for b in boxes if b.content_type == chocolate_ctype]
)
Это должно сделать только 3 запроса (get_for_model
запросы опущены). in_bulk
Метод возвращает вам диктовку в формате {id: model}. Таким образом, чтобы получить ваш content_object вам нужен код вроде:
content_obj = content_objects[box.content_type_id][box.object_id]
Однако я не уверен, будет ли этот код быстрее, чем ваше решение O(5), так как оно требует дополнительной итерации по блокам queryset, а также генерирует запрос с WHERE id IN (...)
заявление
Но если вы сортируете ящики только по полям из модели Box, вы можете заполнить content_objects
dict после нумерации страниц. Но вам нужно пройти content_objects
в __unicode__
как-то
Как бы вы реорганизовали эту схему БД, чтобы упростить такие запросы?
У нас похожая структура. Мы храним content_object
в Box
, но вместо object_id
а также content_object
мы используем ForeignKey(Box)
в Apple
а также Chocolate
, В Box
у нас есть get_object
способ вернуть яблочную или шоколадную модель. В этом случае мы можем использовать select_related
, но в большинстве наших сценариев использования мы фильтруем Boxes по content_type. Так что у нас те же проблемы, что и у вашего 5-го варианта. Но мы начали проект на Django 1.2, когда prefetch_selected не было.
Если вы переименуете ферму / фабрику в какое-то общее имя, например, creator, сработает ли prefetch_related?
О вашем варианте 6
Я могу сказать что-нибудь против заполнения _content_object_cache
, Если вам не нравится иметь дело с внутренностями, вы можете заполнить пользовательское свойство, а затем использовать
apple = getattr(self, 'my_custop_prop', None)
if apple is None:
apple = self.content_object