Django 自递归外键过滤器查询所有子项

问题描述 投票:0回答:10

我有一个带有自引用外键关系的模型:

class Person(TimeStampedModel):
    name = models.CharField(max_length=32)
    parent = models.ForeignKey('self', null=True, blank=True, related_name='children')

现在我想获取一个人的所有多级子级。如何为其编写 Django 查询?它需要表现得像递归函数。

django model recursive-query
10个回答
45
投票

您始终可以向模型添加递归函数:

编辑:根据 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 来代替!


21
投票

您应该阅读有关修改的先序树遍历的内容。 这是 django 的实现。 https://github.com/django-mptt/django-mptt/


8
投票

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

6
投票

如果你知道树的最大深度,你可以尝试这样的事情(未经测试):

Person.objects.filter(Q(parent=my_person)|Q(parent__parent=my_person)| Q(parent__parent__parent=my_person))

4
投票
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()
,直到找不到更多数据/子项。


1
投票

我还将在 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更实用。


1
投票

我知道这已经过时了,但有人可能会得到帮助。

     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
,如果这对您有用),以便可以在任何实例上调用它。


0
投票

我有一个非常相似的业务问题,其中给定一个团队成员,我应该找出他手下的完整团队。但是,拥有大量员工使得递归解决方案效率非常低,而且我的 API 还从服务器收到超时错误。

接受的解决方案采用该节点,转到它的第一个子节点,然后深入到层次结构的底部。然后再次回到第二个孩子(如果存在),然后再次下降直到底部。简而言之,它一一探索所有节点并将所有成员附加到一个数组中。这会导致大量数据库调用,如果需要探索大量节点,则应避免这种情况。我想出的解决方案是逐层获取节点。 db 调用的数量等于层数。看看这个SO链接的解决方案。


0
投票

这是我写的内容,用于获取通过“子”字段连接的帖子的所有步骤

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) 

0
投票

每次调用递归并访问新的子元素时,都会进行数据库调用(即请求的数量将是有多少个子元素)。

为了避免这种情况:您可以从字典创建一个查询集,并基于它,使用

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))
© www.soinside.com 2019 - 2024. All rights reserved.