我有一个带有自引用外键关系的模型:
class Person(TimeStampedModel):
name = models.CharField(max_length=32)
parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
现在我想获取一个人的所有多级子级。如何为其编写 Django 查询?它需要表现得像递归函数。
您始终可以向模型添加递归函数:
编辑:根据 SeomGi Han 更正
def get_all_children(self, include_self=True):
r = []
if include_self:
r.append(self)
for c in Person.objects.filter(parent=self):
_r = c.get_all_children(include_self=True)
if 0 < len(_r):
r.extend(_r)
return r
(如果您有大量递归或数据,请不要使用此...)
仍然按照errx的建议推荐mptt。
编辑:2021 年,因为这个答案仍然受到关注:/
使用 django-tree-queries 来代替!
您应该阅读有关修改的先序树遍历的内容。 这是 django 的实现。 https://github.com/django-mptt/django-mptt/
sunn0 的建议是个好主意,但是 get_all_children() 返回奇怪的结果。它返回类似 [Person1, [Person3, Person4], []] 的内容。应该改为如下所示。
def get_all_children(self, include_self=True):
r = []
if include_self:
r.append(self)
for c in Person.objects.filter(parent=self):
_r = c.get_all_children(include_self=True)
if 0 < len(_r):
r.extend(_r)
return r
如果你知道树的最大深度,你可以尝试这样的事情(未经测试):
Person.objects.filter(Q(parent=my_person)|Q(parent__parent=my_person)| Q(parent__parent__parent=my_person))
class Person(TimeStampedModel):
name = models.CharField(max_length=32)
parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
def get_children(self):
children = list()
children.append(self)
for child in self.children.all():
children.extend(children.get_children())
return children
get_children()
将使用相关名称获取实例的所有子项,然后如果递归找到,它将在子项上调用 get_children()
,直到找不到更多数据/子项。
我还将在 QuerySet 中编写,因为这将允许您链接它们。 我将为所有孩子和所有家长的检索提供答案。
class PersonQuerySet(QuerySet):
def descendants(self, person):
q = Q(pk=person.pk)
for child in person.children.all():
q |= Q(pk__in=self.descendants(child))
return self.filter(q)
def ancestors(self, person):
q = Q(pk=person.pk)
if person.parent:
q |= Q(pk__in=self.ancestors(person.parent))
return self.filter(q)
现在我们需要将
PersonQuerySet
设置为管理员。
class Person(TimeStampedModel):
name = models.CharField(max_length=32)
parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
people = PersonQuerySet.as_manager()
这是最终的查询。
albert_einstein = Person.people.get(name='Albert Einstein')
bernhard_einstein = Person.peole.get(name='Bernhard Caesar Einstein')
einstein_folks = Person.people.descendants(albert_einstein).ancestors(bernhard_einstein)
注意: 以下解决方案与之前的其他答案一样慢。每次递归到其子/父项时,我都会检查数据库命中。 (如果有人可以通过一些优化和缓存进一步改进,这会更好,也许在查询之前prefetch相关数据)。同时mptt更实用。
我知道这已经过时了,但有人可能会得到帮助。
def get_all_children(self, container=None):
if container is None:
container = []
result = container
for child in self.children.all():
result.append(child)
if child.children.count() > 0:
child.get_all_children(result)
return result
然后简单地将其设为模型上的
property
(或者 cached_property
,如果这对您有用),以便可以在任何实例上调用它。
这是我写的内容,用于获取通过“子”字段连接的帖子的所有步骤
steps = Steps.objects.filter(post = post)
steps_ids = steps.values_list('id', flat=True)
递归获取“steps”的所有子节点:
objects = Steps.objects.filter(child__in=steps_ids)
while objects:
steps = steps | objects
steps_ids = objects.values_list('id', flat=True)
objects = Steps.objects.filter(child__in=steps_ids)
每次调用递归并访问新的子元素时,都会进行数据库调用(即请求的数量将是有多少个子元素)。
为了避免这种情况:您可以从字典创建一个查询集,并基于它,使用
parent__name
键创建一个字典。使用此键,递归选择子元素:
def children_list(self):
data = Person.objects.values('name', 'parent__name')
result = {}
for entry in data:
result.setdefault(entry['parent__name'], []).append(entry)
def generate_inside(name):
arr = []
if name in result:
for entry in result[name]:
arr.append(entry['name'])
resalt_recursion = generate_inside(entry['name'])
arr.extend(resalt_recursion)
return arr
return generate_inside(self.name)
检查数据库的命中数(运行shell):
python manage.py shell
连接所有必要的模块:
from menu.models import Person
from django.db import connection
from django.db import reset_queries
并提出请求:
reset_queries()
print(Person.objects.all()[0].children_list())
print('how many queries', len(connection.queries))