通过反向外键注释 Django 查询集

Annotating Django query sets through reverse foreign keys

给定一组简单的模型如下:

class A(models.Model):
    pass

class B(models.Model):
    parent = models.ForeignKey(A, related_name='b_set')

class C(models.Model):
    parent = models.ForeignKey(B, related_name='c_set')

我想创建一个带有两个注释的 A 模型的查询集。一个注释应该是 B 行的数量,其中 A 行作为其父级。另一个注释应该表示 B 行的数量,再次以 A 对象作为父对象,它们的 [= 中至少有 nC 类型的对象21=].

例如,考虑以下数据库和n = 3:

Table A
id
0
1

Table B
id  parent
0   0
1   0

Table C
id parent
0   0
1   0
2   1
3   1
4   1

我希望能够得到 [(0, 2, 1), (1, 0, 0)] 形式的结果,因为 ID 为 0 的 A 对象有两个 B 对象,其中一个对象至少有三个相关 C 个对象。 ID 为 1 的 A 对象没有 B 个对象,因此也没有 B 个至少有三个 C 行的对象。

第一个注释很简单:

A.objects.annotate(annotation_1=Count('b_set'))

我现在要设计的是第二个注解。我设法计算了每个 AB 行数,其中 B 对象至少有一个 C 对象,如下所示:

A.objects.annotate(annotation_2=Count('b_set__c_set__parent', distinct=True))

但我想不出一种方法来使用除一个之外的最小相关集大小。希望这里有人能指出我正确的方向。我想到的一种方法是以某种方式注释查询中的 B 对象而不是 A 行,这是注释方法的默认设置,但我找不到任何关于此的资源。

这是 Django 1.11 限制下的复杂查询。我决定通过两个查询来完成,并将结果合并到一个列表中,该列表可以像查询集一样被视图使用:

from django.db.models import Count

sub_qs = (
    C.objects
    .values('parent')
    .annotate(c_count=Count('id'))
    .order_by()
    .filter(c_count__gte=n)
    .values('parent')
)
qs = B.objects.filter(id__in=sub_qs).values('parent_id').annotate(cnt=Count('id'))
qs_map = {x['parent_id']: x['cnt'] for x in qs}
rows = list(A.objects.annotate(annotation_1=Count('b_set')))
for row in rows:
    row.annotation_2 = qs_map.get(row.id, 0)

列表rows就是结果。比较复杂的qs.query编译成相对简单的SQL:

>>> print(str(qs.query))
SELECT app_b.parent_id, COUNT(app_b.id) AS cnt
FROM app_b
WHERE app_b.id IN (
    SELECT U0.parent_id AS Col1 FROM app_c U0
    GROUP BY U0.parent_id HAVING COUNT(U0.id) >= 3
)
GROUP BY app_b.parent_id;                -- (added white space and removed double quotes)

这个简单的解决方案可以更容易地修改和测试。


注意:一个查询的解决方案也存在,但似乎没有用。原因:需要 SubqueryOuterRef()。它们很棒,但是通常 Count() 来自聚合的查询不支持与连接解析一起编译的查询。一个子查询可以通过lookup ...__in=... 来分离,可以被Django 编译,但是这样就无法使用OuterRef()。如果它是在没有 OuterRef() 的情况下编写的,那么它是一个非常复杂的非最佳嵌套 SQL 时间复杂度可能是 O(n2) A table 用于许多(或所有)数据库后端。未测试。