Django结合MPTT时,在ListCreateAPIView的get_queryset中向annotate子查询传参失败的问题
看起来你踩了一个Django ORM和MPTT结合的常见坑——OuterRef只能在查询表达式层面延迟解析,但你在subqueryNested里直接调用了ItemCategory.objects.get(pk=OuterRef('id')).get_descendants(),这里的get()方法会尝试立即执行查询,而OuterRef是依赖外层查询上下文的延迟引用,根本没法在这个场景下工作,所以才会抛出那个“只能在子查询中使用外部引用”的错误。
先拆解一下问题:你的subqueryItem能正常工作,是因为它完全在ORM查询链里处理OuterRef('id'),没有触发任何立即执行的操作;但subqueryNested里的get_descendants()是MPTT模型的实例方法,必须先拿到具体的ItemCategory实例才能调用,而用OuterRef的话,根本没法提前获取到这个实例,自然就冲突了。
下面给你两种可行的解决方案,都是把逻辑拉回QuerySet层面,让Django能正确处理外层查询和子查询的关联:
方案一:直接利用MPTT的核心字段(lft/rgt)构建查询
MPTT是通过lft和rgt两个字段维护树形结构的,父节点的lft小于所有后代的lft,rgt大于所有后代的rgt,我们可以直接用这个特性来匹配当前节点的整个子树:
def get_queryset(self): queryset = super().get_queryset() subqueryItem = Subquery( Item.objects.filter(category=OuterRef('id')) .order_by() .values('category') .annotate(count=Count('pk')) .values('count'), output_field=IntegerField() ) # 重写subqueryNested,用lft/rgt范围匹配子树 subqueryNested = Subquery( Item.objects.filter( category__in=ItemCategory.objects.filter( lft__gte=OuterRef('lft'), rgt__lte=OuterRef('rgt') ) ) .order_by() .values('category') .annotate(count=Count('pk')) .values('count'), output_field=IntegerField() ) queryset = queryset.annotate( collcount=Coalesce(subqueryItem, 0), collcountdeep=Coalesce(subqueryNested, 0), ) return queryset
方案二:用MPTT的QuerySet方法替代实例方法
如果你更习惯用MPTT提供的API,可以用TreeManager的get_descendants_queryset方法——它返回的是一个未执行的QuerySet,能和OuterRef完美配合:
def get_queryset(self): queryset = super().get_queryset() subqueryItem = Subquery( Item.objects.filter(category=OuterRef('id')) .order_by() .values('category') .annotate(count=Count('pk')) .values('count'), output_field=IntegerField() ) # 用MPTT的QuerySet方法获取子树 subqueryNested = Subquery( Item.objects.filter( category__in=ItemCategory.objects.filter(pk=OuterRef('id')).get_descendants_queryset(include_self=True) ) .order_by() .values('category') .annotate(count=Count('pk')) .values('count'), output_field=IntegerField() ) queryset = queryset.annotate( collcount=Coalesce(subqueryItem, 0), collcountdeep=Coalesce(subqueryNested, 0), ) return queryset
核心思路就是:所有涉及OuterRef的逻辑都要保持在QuerySet层面,不要调用任何会立即执行查询的实例方法(比如get()、first()),这样Django才能正确处理外层查询和子查询的关联关系。
备注:内容来源于stack exchange,提问作者oscurodrago

