如何过滤 Django 查询的连接表,然后在一个查询中迭代连接表?

Posted

技术标签:

【中文标题】如何过滤 Django 查询的连接表,然后在一个查询中迭代连接表?【英文标题】:How can you filter a Django query's joined tables then iterate the joined tables in one query? 【发布时间】:2020-08-26 09:23:01 【问题描述】:

我有一个表 Parent 和一个表 Child,它有一个表 Parent 的外键。

我想查询所有有一个孩子叫 Eric 的父母,并报告 Eric 的年龄。

我跑:

parents = Parents.objects.filter(child__name='Eric')

然后我遍历查询集:

for parent in parents:
    print(f'Parent name parent.name child Eric age parent.child.age')

显然这不起作用 - 我需要通过外键对象管理器访问 child,所以我尝试:

for parent in parents:
    print(f'Parent name parent.name')
    for child in parent.child_set.all():
        print(f'Child Eric age parent.child.age')

Django 返回所有孩子的年龄,而不仅仅是名为 Eric 的孩子。

我可以重复过滤条件:

parents = Parents.objects.filter(child__name='Eric')
for parent in parents:
    print(f'Parent name parent.name')
    for child in parent.child_set.filter(name='Eric'):
        print(f'Child Eric age child.age')

但这意味着重复的代码(因此当另一个开发人员对一个而不是另一个进行更改时,未来可能会出现不一致),并在数据库上运行第二个查询。

有没有办法获取匹配的记录并对其进行迭代?多年来一直在 Djangoing,不敢相信我不能这样做!

PS。我知道我可以做到Child.objects.filter(name='Eric').select_related('parent')。但我真正想做的涉及第二个子表。因此,在上面的示例中添加一个带有外键的表地址到父级。我想让有孩子的父母叫 Eric,地址在 Timbuktu,并遍历所有 Timbuktu 地址和所有小 Eric。这就是我不想使用 Child 的对象管理器的原因。

这是我能想到的最好的方法 - 三个查询,重复每个过滤器。

children = Children.objects.filter(name='Eric')
addresses = Address.objects.filter(town='Timbuktu')
parents=(
    Parent.objects
        .filter(child__name='Eric', address__town='Timbuktu')
        .prefetch_related(Prefetch('child_set', children))
        .prefetch_related(Prefetch('address_set', addresses))
)

【问题讨论】:

你想遍历没有匹配孩子的父母吗? 您只想遍历Child 表吗?您的 Address 示例可以通过子查询 Child.objects.filter(name='Eric', parent__in=Parent.objects.filter(address__country='Timbuktu')).select_related('parent') 解决 是否有特殊原因需要单个查询?你可以使用prefetch_related 并且只执行 2 你想避免重复代码吗? 3 次查询似乎并不过分 公平地说,我认为您可以使用 values() 查询集在 1 个查询中获取您需要的所有数据。唯一的问题是你会有重复的数据,你去重复和迭代的逻辑会有点混乱,尽管重复的查询解决方案不是“干净的” 【参考方案1】:

.values 函数让您可以直接访问返回的记录集(感谢@Iain Shelvington):

parents_queryset_dicts = Parent.objects
    .filter(child__name='Eric', address__town='Timbuktu')
    .values('id', 'name', 'child__id', 'address__id', 'child__age', 'address__whatever')
    .order_by('id', 'child__id', 'address__id')

请注意,虽然这会检索子项和地址的笛卡尔积,但我们在减少查询次数方面的收益会被下面的双倍大小的结果集和重复数据删除略微抵消。所以我开始认为使用Child.objectsAddress.objects 的两个查询更好——稍慢但更简单的代码。

在我的实际用例中,我有多个外键连接的多表链,因此拆分查询以防止笛卡尔连接,但仍使用.values() 方法来获取过滤的嵌套表。

如果您想要一个层次结构,例如,作为 JSON 发送到客户端,则生成:

parents = 
    parent_id: 
        'name': name, 
        'children': 
            child_id: 
                'age': child_age
            ,
        'addresses': 
            address_id: 
                'whatever': address_whatever
            ,
        ,
    ,

运行类似:

prev_parent_id = prev_child_id = prev_address_id = None
parents = 
for parent in parents_queryset_dicts:
    if parent['id'] != prev_parent_id:
        parents[parent['id']] = 'name': parent['name'], children: , addresses: 
        prev_parent_id = parent['id']
    if parent['child__id'] != prev_child_id:
        parents[parent['id']]['children'][parent['child__id']] = 'age': parent['child__age']
        prev_child_id = parent['child__id']
    if parent['address__id'] != prev_address_id:
        parents[parent['id']]['addresses'][parent['address__id']] = 'whatever': parent['address__whatever']
        prev_address_id = parent['address__id']

这是密集代码,您不再可以访问任何未显式提取和复制的字段,包括任何嵌套的~_set 查询集,并且笛卡尔积的重复数据删除对后来的开发人员来说并不明显。您可以获取查询集,保留它,然后提取.values,这样您就可以从同一个单一的数据库查询中获得两者。但是通常三个查询重复过滤器会更干净一些,如果几个数据库查询效率较低:

children = Children.objects.filter(name='Eric')
addresses = Address.objects.filter(town='Timbuktu')
parents_queryset = (
    Parent.objects
        .filter(child__name='Eric', address__town='Timbuktu')
        .prefetch_related(Prefetch('child_set', children))
        .prefetch_related(Prefetch('address_set', addresses))
)

parents = 
for parent in parents_queryset:
    parents[parent.id] = 'name': parent['name'], children: , addresses: 
    for child in parent.child_set:  # this is implicitly filtered
        parents[parent.id]['children'][child.id] = 'age': child.age
    for address in parent.address_set:  # also implicitly filtered
        parents[parent.id]['addresses'][address.id] = 'whatever': address.whatever

最后一种方法是使用annotateF() 对象。我没有对此进行过实验,虽然生成的 SQL 看起来不错,而且它似乎运行单个查询并且不需要重复过滤器:

from django.db.models import F
parents = (
    Parent.objects.filter(child__name='Eric')
    .annotate(child_age=F('child__age'))
)

优点和缺点似乎与上面的.values() 相同,尽管.values() 似乎稍微更基本的Django(因此更易于阅读)并且您不必重复字段名称(例如,上面的child_age=child__age 的混淆)。优点可能是. 访问器而不是['field'] 的便利性,您保留了惰性嵌套记录集等 - 尽管如果您正在计算查询,您可能希望在每行发出意外查询时事情会失败.

【讨论】:

以上是关于如何过滤 Django 查询的连接表,然后在一个查询中迭代连接表?的主要内容,如果未能解决你的问题,请参考以下文章

django ORM model filter 条件过滤,及多表连接查询反向查询,某字段的distinct

mysql左连接没有数据还会查出来吗

mysql中,如何向测试人员介绍连接查询和子查询的优劣势?

如何使用 StringAgg 或 ArrayAgg 连接多个子行中的一列来注释 django 查询集?

django 过滤查询

如何在查询集中动态设置 Django 过滤器