17

假设我有一个模型BoxGenericForeignKey它指向一个Apple实例或一个Chocolate实例。AppleChocolate又分别具有到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)

这是我尝试过的几件事。在所有这些示例中,N是 Box 的数量。查询计数假定ContentTypesAppleChocolate已经被缓存,因此get_for_model()调用不会命中数据库。

1)天真:

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

这会执行1(获取 Boxes)+ N(为每个 Box 获取 Apple 或 Chocolate)+ N(为每个 Apple 获取 Farm,为每个 Chocolate 获取 Factory)查询。

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

3) 从 django 1.4 开始,prefetch_related可以 fetch GenericForeignKeys。

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

这会执行1(获取盒子)+ 2(获取所有盒子的苹果和巧克力)+ N(获取每个苹果的农场和每个巧克力的工厂)查询。

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')

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

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

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

加分项:您将如何重构此 DB 模式以使此类查询更容易?

4

1 回答 1

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} 的字典。因此,要获取您的 content_object,您需要如下代码:

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

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

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

您将如何重构此 DB 模式以使此类查询更容易?

我们有类似的结构。我们存储content_object在 中Box,但我们使用and而不是object_idand 。我们有一个返回 Apple 或 Chocolate 模型的方法。在这种情况下,我们可以使用,但在大多数用例中,我们通过 content_type 过滤 Boxes。因此,我们遇到了与您的第 5 个选项相同的问题。但是当没有 prefetch_selected 时,我们在 Django 1.2 上开始了我们的项目。content_objectForeignKey(Box)AppleChocolateBoxget_objectselect_related

如果您将农场/工厂重命名为某个通用名称,例如创建者,prefetch_related 会起作用吗?

关于您的选择 6

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

apple = getattr(self, 'my_custop_prop', None)
if apple is None:
    apple = self.content_object
于 2012-10-19T14:10:57.330 回答