You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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.19
  • django-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(),
    )
)

逻辑说明:

  1. node_range子查询:获取外部查询中当前Model1节点的lft和rgt值。
  2. 在Model2的过滤条件中,通过model1__lft__gte和model1__rgt__lte筛选出所有关联到当前节点后代(含自身)的Model2记录。
  3. 最后按model1分组统计数量,得到每个Model1节点对应的可见Model2总数。

这个方案完全等价于get_queryset_descendants的功能,同时保留了子查询结构,支持OuterRef的使用,完美适配你的需求。

内容的提问来源于stack exchange,提问作者Nobilissimum

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 08:57:52