Предположим, что у меня есть модель 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) лучшим, что я могу сделать, или есть более простое решение?
Бонусные баллы: Как бы вы реорганизовали эту схему БД, чтобы упростить такие запросы?