Подтвердить что ты не робот

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 (выберите Apple или Chocolate для каждого окна) + N (выберите ферму для каждого Apple и Factory для каждого Chocolate).

2) select_related здесь не помогает, потому что Box.content_object является GenericForeignKey.

3) Что касается django 1.4, prefetch_related может получить GenericForeignKey s.

print [box for box in Box.objects.prefetch_related('content_object').all()]

Это делает 1 (выборка) + 2 (выборки яблок и конфет для всех ящиков) + N (выборка Ферма для каждого Apple и Factory для каждого Chocolate).

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) вручную. В моем реальном приложении я показываю эти Boxes в разбитом на страницы ModelAdmin. Неясно, как интегрировать это решение там. Может быть, я могу написать пользовательский Paginator для прозрачного кеширования?

6) Я мог бы согнуть что-то, основанное на этом, которое также выполняет O (1) запросы. Но я бы предпочел не возиться с внутренними элементами (_content_object_cache), если я могу избежать этого.

Вкратце: Печать ящика требует доступа к ForeignKeys GenericForeignKey. Как я могу напечатать N ящиков в O (1) запросах? Является ли (5) лучшим, что я могу сделать, или есть более простое решение?

Бонусные баллы: Как бы вы реорганизовали эту схему БД, чтобы упростить такие запросы?

4b9b3361

Ответ 1

Вы можете вручную реализовать что-то вроде prefetch_selected и использовать метод Django 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 возвращает вам dict в формате {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 для возврата модели Apple или Chocolate. В этом случае мы можем использовать select_related, но в большинстве наших случаев использования мы фильтруем Boxes по контенту_type. Таким образом, у нас есть те же проблемы, что и ваш 5-й вариант. Но мы начали проект Django 1.2, когда не было prefetch_selected.

Если вы переименовали farm/ factory в какое-то общее имя, например создатель, выполнили ли prefetch_related работу?

О вашей опции 6

Я могу сказать что-либо против заполнения _content_object_cache. Если вам не нравится работать с внутренними компонентами, вы можете заполнить собственное свойство, а затем использовать

apple = getattr(self, 'my_custop_prop', None)
if apple is None:
    apple = self.content_object