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
Другие вопросы по тегам