django:预取GenericForeignKey的相关对象

时间:2012-09-17 20:51:25

标签: django optimization foreign-keys generic-foreign-key

假设我的模型Box的{​​{1}}指向GenericForeignKey实例或Apple实例。反过来,ChocolateApple分别将ForeignKeys设为ChocolateFarm。我想显示Factory es列表,我需要访问BoxFarm。如何在尽可能少的数据库查询中执行此操作?

最小的说明性示例:

Factory

以下是我尝试过的一些事情。在所有这些示例中, N 是Box的数量。查询计数假定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) ContentType的{​​{1}}已经被缓存,因此Apple调用不会访问数据库。

1)天真:

Chocolate

1 (获取框)+ N (每个Box获取Apple或巧克力)+ N (每个Apple获取一个Farm)和每个巧克力的工厂查询。

2)get_for_model()在这里没有帮助,因为print [box for box in Box.objects.all()]select_related

3)从django 1.4开始,Box.content_object可以获取GenericForeignKey s。

prefetch_related

1 (获取框)+ 2 (为所有框提取苹果和巧克力)+ N (每个Apple获取一个农场)和每个巧克力的工厂查询。

4)显然GenericForeignKey不够智能,无法关注GenericForeignKeys的ForeignKeys。如果我尝试:

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

理所当然地抱怨prefetch_related个对象没有print [box for box in Box.objects.prefetch_related( 'content_object__farm', 'content_object__factory').all()]字段,反之亦然。

5)我能做到:

Chocolate

1 (获取框)+ 2 (为所有框提取苹果和巧克力)+ 2 (为所有苹果提取农场)所有巧克力的工厂和工厂查询。缺点是我必须手动合并和排序两个查询集(farmapple_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') )。在我的实际应用程序中,我将在分页的ModelAdmin中显示这些Box。如何在此处集成此解决方案并不明显。也许我可以写一个自定义Paginator来透明地进行缓存?

6)我可以根据this拼凑一些也可以进行O(1)查询的东西。但是如果我能避免的话,我宁愿不要弄乱内部(boxes_with_apples)。

总结:打印Box需要访问GenericForeignKey的ForeignKeys。如何在O(1)查询中打印N个框?(5)我能做的最好,还是有更简单的解决方案?

奖励积分:您如何重构此数据库架构以简化此类查询?

1 个答案:

答案 0 :(得分:10)

您可以手动实现类似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方法以{id:model}格式返回一个dict。因此,要获取content_object,您需要一个代码:

content_obj = content_objects[box.content_type_id][box.object_id]

但是,我不确定此代码是否会比 O(5)解决方案更快,因为它需要对box queryset进行额外迭代,并且还会生成带WHERE id IN (...)语句的查询< / p>

但是,如果您仅使用Box模型中的字段对框进行排序,则可以在分页后填充content_objects dict。但是你需要以某种方式将content_objects传递给__unicode__

  

您如何重构此数据库架构以简化此类查询?

我们有类似的结构。我们将content_object存储在Box中,但我们使用object_idcontent_object中的ForeignKey(Box)代替AppleChocolate。在Box中,我们使用get_object方法返回Apple或巧克力模型。在这种情况下,我们可以使用select_related,但在大多数用例中,我们按content_type过滤Boxes。所以我们遇到了像你的第五个选择一样的问题。但是,当没有prefetch_selected时,我们开始在Django 1.2上进行项目。

如果您将场/工厂重命名为某个通用名称(如创建者),是否会预取相关的工作?

关于选项6

我可以说任何反对填充_content_object_cache的内容。 如果您不喜欢处理内部问题,可以填写自定义属性,然后使用

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