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

Django闭包表架构下重写get_children方法解决递归渲染报错

闭包表树形结构渲染的无限递归问题解决

我在Django项目里用闭包表(Closure Table)实现文档树形结构,模型是Document和DocumentClosure(后者存储祖先与后代的关系)。现在想模仿django-mptt的效果,用自定义的recursetree模板标签渲染树形结构,但当前的get_children方法导致无限数据库查询,最终触发了maximum recursion depth exceeded错误。下面是相关代码、报错信息和示例数据,以及解决方案:

相关代码

models.py

class Document(models.Model):
    name = models.TextField(blank=True, null=True)
    def get_children(self):
        return [dc.child for dc in DocumentClosure.objects.filter(parent=self)]

class DocumentClosure(models.Model):
    parent = models.ForeignKey(
        Document, on_delete=models.CASCADE, related_name="closures_as_parent", db_column='parent_id', blank=True, null=True
    )
    child = models.ForeignKey(
        Document, on_delete=models.CASCADE, related_name="closures_as_child", db_column='child_id', blank=True, null=True
    )
    level = models.IntegerField(
        default=0, blank=True, null=True
    )

模板代码

<ul>
    {% recursetree documents %}
    <li>
        {{ node.name }}
        <ul class="children">
            {{ children }}
        </ul>
    </li>
    {% endrecursetree %}
</ul>

custom_tag.py

from django import template
from django.utils.safestring import mark_safe

register = template.Library()

@register.tag
def recursetree(parser, token):
    bits = token.contents.split()
    if len(bits) != 2:
        raise template.TemplateSyntaxError(_('%s tag requires a queryset') % bits[0])
    queryset_var = template.Variable(bits[1])
    template_nodes = parser.parse(('endrecursetree',))
    parser.delete_first_token()
    return RecurseTreeNode(template_nodes, queryset_var)

class RecurseTreeNode(template.Node):
    def __init__(self, template_nodes, queryset_var):
        self.template_nodes = template_nodes
        self.queryset_var = queryset_var

    def _render_node(self, context, node):
        bits = []
        context.push()
        for child in node.get_children():
            bits.append(self._render_node(context, child))
        context['node'] = node
        context['children'] = mark_safe(''.join(bits))
        rendered = self.template_nodes.render(context)
        context.pop()
        return rendered

    def render(self, context):
        queryset = self.queryset_var.resolve(context)
        roots = queryset
        bits = [self._render_node(context, node) for node in roots]
        return ''.join(bits)

报错信息

RuntimeError: maximum recursion depth exceeded in cmp

示例数据

树形结构

A
| - B
|   | - C

document表

idparent_idname
1A
21B
32C

document_closure表

idparent_idchild_idlevel
1110
2220
3330
4121
5231
6132

问题分析与解决方案

问题根源

你的DocumentClosure表中每个节点都有一条自身到自身的记录(level=0),但原来的get_children方法没有过滤这条记录。这导致每个节点的子列表里会包含自己,模板递归渲染时就会不断调用自身的get_children方法,最终触发递归深度超限的错误。另外,原方法每次调用都会触发新的数据库查询,还存在N+1查询的性能问题。

修改后的get_children方法

我们需要过滤出直接子节点(即level=1的记录),同时优化数据库查询减少性能损耗。下面是两种可行的写法:

写法1:通过DocumentClosure关联查询(推荐)

class Document(models.Model):
    name = models.TextField(blank=True, null=True)
    
    def get_children(self):
        # 过滤直接子节点(level=1),并预加载child对象避免N+1查询
        return [dc.child for dc in self.closures_as_parent.filter(level=1).select_related('child')]

写法2:直接查询Document对象

class Document(models.Model):
    name = models.TextField(blank=True, null=True)
    
    def get_children(self):
        # 通过反向关联直接筛选出当前节点的直接子节点
        return Document.objects.filter(
            closures_as_child__parent=self,
            closures_as_child__level=1
        ).distinct()

为什么这样解决?

  1. 过滤level=1:确保只获取当前节点的直接子节点,排除自身的level=0记录,彻底终止无限递归。
  2. select_related('child'):一次性加载关联的child Document对象,避免每次访问dc.child时触发新的数据库查询,解决N+1查询的性能问题。
  3. distinct():防止因闭包表数据异常导致的重复子节点(如果闭包表维护正确,这一步可以省略,但加上更稳妥)。

额外提示

确保你的闭包表数据维护正确:当添加/删除子节点时,要同步更新所有相关的祖先-后代关系及level值。比如添加C作为B的子节点时,需要生成:

  • B→C(level=1)
  • A→C(level=2)
  • C→C(level=0)

这样才能保证闭包表的关系是完整且正确的。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:58:03