如何在Django MPTT中用子查询调用get_queryset_descendants并支持OuterRef
我需要实现和get_queryset_descendants(queryset, include_self=True)完全相同的查询结果,但要求传入的queryset保留为子查询结构,不被提前解析成具体的ID集合。因为get_queryset_descendants的内部机制会直接解析传入的queryset,导致无法将其作为子查询的一部分使用,也就没法结合OuterRef("pk")实现关联外部查询的逻辑。
举个例子,下面的代码里Model.objects.filter(id=1)并没有以子查询的形式存在,最终生成的SQL里看不到这个过滤条件:
descendants = Model.tree.get_queryset_descendants( Model.objects.filter(id=1), include_self=True, ) print(str(descendants.query)) # 输出的SQL里没有id=1的过滤逻辑
我的模型定义如下:
import django import mptt class Model1(mptt.models.MPTTModel): objects = django.db.models.Manager() tree = mptt.managers.TreeManager() class Model2(django.db.models.Model): model1 = django.db.models.ForeignKey(Model1)
我想要实现的功能是:统计每个Model1节点及其所有后代节点关联的可见Model2数量。尝试用Subquery结合get_queryset_descendants时,传入Model1.objects.filter(id=OuterRef("pk"))会报错,因为get_queryset_descendants会解析这个queryset,而OuterRef只能在子查询中使用:
from django.db.models import ( Count, IntegerField, Subquery, ) model1_with_visible_model2 = Model1.objects.annotate( visible_model2_count=Subquery( ( Model2.objects .filter( model1__id__in=( # 这里不能直接用model1=OuterRef("pk"),因为要统计后代节点的Model2 Model1.tree.get_queryset_descendants( Model1.objects.filter( # id=6347, # 写具体ID时正常 id=OuterRef("pk"), # 这是我想要实现的逻辑,但会报错 ).all(), include_self=True, ) ).values_list("id", flat=True), is_visible=True, ) .distinct() .values("model1") .annotate(count=Count("pk")) .values("count") ), output_field=IntegerField(), ), )
我使用的版本:
Django==3.2.19django-mptt==0.11.0
由于get_queryset_descendants无法保留传入的子查询结构,我们可以手动利用MPTT的左右值(lft/rgt)机制来实现等价的查询逻辑,这样就能保留OuterRef的关联。
MPTT的核心原理是用lft和rgt字段标记节点的范围:一个节点的所有后代节点的lft值都在该节点的lft和rgt之间。所以我们可以通过子查询获取当前外部节点的lft和rgt,然后过滤出所有满足lft__gte=目标lft且rgt__lte=目标rgt的Model1节点,这就等价于get_queryset_descendants(include_self=True)的结果。
修改后的代码如下:
from django.db.models import ( Count, IntegerField, Subquery, OuterRef, ) # 先定义子查询,获取当前Model1节点的lft和rgt范围 node_range = Model1.objects.filter( id=OuterRef("pk") ).values("lft", "rgt") model1_with_visible_model2 = Model1.objects.annotate( visible_model2_count=Subquery( Model2.objects.filter( # 过滤出关联的Model1是当前节点或其后代的记录 model1__lft__gte=Subquery(node_range.values("lft")), model1__rgt__lte=Subquery(node_range.values("rgt")), is_visible=True, ) # 按model1分组统计数量 .values("model1") .annotate(count=Count("pk")) .values("count"), output_field=IntegerField(), ) )
逻辑说明:
node_range子查询:获取外部查询中当前Model1节点的lft和rgt值。- 在Model2的过滤条件中,通过
model1__lft__gte和model1__rgt__lte筛选出所有关联到当前节点后代(含自身)的Model2记录。 - 最后按
model1分组统计数量,得到每个Model1节点对应的可见Model2总数。
这个方案完全等价于get_queryset_descendants的功能,同时保留了子查询结构,支持OuterRef的使用,完美适配你的需求。
内容的提问来源于stack exchange,提问作者Nobilissimum

