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表
| id | parent_id | name |
|---|---|---|
| 1 | A | |
| 2 | 1 | B |
| 3 | 2 | C |
document_closure表
| id | parent_id | child_id | level |
|---|---|---|---|
| 1 | 1 | 1 | 0 |
| 2 | 2 | 2 | 0 |
| 3 | 3 | 3 | 0 |
| 4 | 1 | 2 | 1 |
| 5 | 2 | 3 | 1 |
| 6 | 1 | 3 | 2 |
问题分析与解决方案
问题根源
你的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()
为什么这样解决?
- 过滤
level=1:确保只获取当前节点的直接子节点,排除自身的level=0记录,彻底终止无限递归。 select_related('child'):一次性加载关联的childDocument对象,避免每次访问dc.child时触发新的数据库查询,解决N+1查询的性能问题。distinct():防止因闭包表数据异常导致的重复子节点(如果闭包表维护正确,这一步可以省略,但加上更稳妥)。
额外提示
确保你的闭包表数据维护正确:当添加/删除子节点时,要同步更新所有相关的祖先-后代关系及level值。比如添加C作为B的子节点时,需要生成:
- B→C(level=1)
- A→C(level=2)
- C→C(level=0)
这样才能保证闭包表的关系是完整且正确的。
内容的提问来源于stack exchange,提问作者Nurzhan Nogerbek
相关产品推荐
相关产品推荐

