django:预取GenericForeignKey的相关对象

cbe*_*zan 16 django optimization foreign-keys generic-foreign-key

假设我有一个模型,Box一个GenericForeignKey指向任何一个Apple实例或Chocolate实例.AppleChocolate反过来,有ForeignKeys来FarmFactory,分别.我想显示一个Boxes 列表,我需要访问FarmFactory.如何在尽可能少的数据库查询中执行此操作?

最小的说明性示例:

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)
Run Code Online (Sandbox Code Playgroud)

这是我尝试过的一些事情.在所有这些例子中,N是Box的数量.查询计数假定ContentTypes for AppleChocolate已经被缓存,因此get_for_model()调用不会命中DB.

1)天真:

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

这样做1(获取盒子)+ N(每个盒子取苹果或巧克力)+ N(每个巧克力的每个Apple和Factory获取Farm)查询.

2)select_related在这里没有帮助,因为Box.content_objectGenericForeignKey.

3)从django 1.4开始,prefetch_related可以获取GenericForeignKeys.

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

这样做1(获取盒子)+ 2(为所有盒子提取苹果和巧克力)+ N(每个巧克力的每个Apple和Factory获取Farm)查询.

4)显然prefetch_related不够聪明,不能关注GenericForeignKeys的ForeignKeys.如果我尝试:

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')
Run Code Online (Sandbox Code Playgroud)

这样做1(获取盒子)+ 2(为所有盒子提取苹果和巧克力)+ 2(为所有巧克力的所有苹果和工厂提取农场)查询.缺点是我必须手动合并和排序两个查询集(boxes_with_apples,boxes_with_chocolates).在我的实际应用程序中,我将在分页的ModelAdmin中显示这些Box.如何在此处集成此解决方案并不明显.也许我可以编写一个自定义Paginator来透明地执行此缓存?

6)我可以拼凑一些基于此的东西也可以进行O(1)查询.但是_content_object_cache如果我能避免它,我宁愿不要弄乱内部().

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

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

Igo*_*gor 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]
    )
Run Code Online (Sandbox Code Playgroud)

这应该只进行3次查询(get_for_model省略查询).该in_bulk方法以{id:model}格式返回一个dict.因此,要获取content_object,您需要以下代码:

content_obj = content_objects[box.content_type_id][box.object_id]
Run Code Online (Sandbox Code Playgroud)

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

但是如果你只用Box模型中的字段对盒子进行排序,你可以content_objects在分页后填写dict.但你需要content_objects__unicode__某种方式传递

您将如何重构此数据库架构以使此类查询更容易?

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

如果你将farm/factory重命名为某个通用名称,比如creator,会prefetch_related工作吗?

关于您的选择6

我可以说反对填充_content_object_cache.如果您不喜欢处理内部,您可以填写自定义属性然后使用

apple = getattr(self, 'my_custop_prop', None)
if apple is None:
    apple = self.content_object
Run Code Online (Sandbox Code Playgroud)