如何使用 django-treebeard 的 MP_Node 预取后代?
How to prefetch descendants with django-treebeard's MP_Node?
我正在使用 django-treebeard
在 django-rest-framework
中开发具有分层数据结构的应用程序。我的(简化的)主要模型看起来像这样
class Task(MP_Node):
name = models.CharField(_('name'), max_length=64)
started = models.BooleanField(default=True)
我目前正在尝试实现的是所有根节点的列表视图,它显示额外的字段(例如是否所有子节点都已启动)。为此,我指定了一个视图:
class TaskViewSet(viewsets.ViewSet):
def retrieve(self, request, pk=None):
queryset = Task.get_tree().filter(depth=1, job__isnull=True)
operation = get_object_or_404(queryset, pk=pk)
serializer = TaskSerializer(operation)
return Response(serializer.data)
和序列化程序
class TaskSerializer(serializers.ModelSerializer):
are_children_started = serializers.SerializerMethodField()
def get_are_children_started(self, obj):
return all(task.started for task in Task.get_tree(obj))
一切正常,我得到了预期的结果。但是,我 运行 遇到了一个 N+1 查询问题,对于每个根任务,我需要分别获取所有子任务。通常这可以使用 prefetch_related
解决,但是当我使用 django-treebeard
中的物化路径结构时,任务模型之间没有 Django 关系,所以 prefetch_related
不知道该怎么做盒子。我试过使用自定义 Prefetch 对象,但由于这仍然需要 Django 关系路径,所以我无法让它工作。
我目前的想法是使用指向其根节点的外键扩展任务模型,如下所示:
root_node = models.ForeignKey('self', null=True,
related_name='descendant_tasks',
verbose_name=_('root task')
)
为了明确MP关系,方便查询。然而,这确实感觉有点不干脆,所以我想知道是否有人对如何解决它有其他建议。
最后我确实为每个指向其根节点的任务添加了一个外键,如下所示:
root_node = models.ForeignKey('self', null=True,
related_name='descendant_tasks',
verbose_name=_('root task')
)
我更新了任务模型上的保存方法,以确保我始终指向正确的根节点
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
try:
self.root_task = self.get_root()
except ObjectDoesNotExist:
self.root_task = None
return super(Task, self).save(force_insert=False, force_update=False, using=None,
update_fields=None
)
这让我可以使用 prefetch_related('descendants')
简单地预取所有后代。
每当我需要以嵌套方式拥有后代时,我都会使用以下函数再次嵌套扁平化的后代列表
def build_nested(tasks):
def get_basepath(path, depth):
return path[0:depth * Task.steplen]
container, link = [], {}
for task in sorted(tasks, key=attrgetter('depth')):
depth = int(len(task.path) / Task.steplen)
try:
parent_path = get_basepath(task.path, depth - 1)
parent_obj = link[parent_path]
if not hasattr(parent_obj, 'sub_tasks'):
parent_obj.sub_tasks = []
parent_obj.sub_tasks.append(task)
except KeyError: # Append it as root task if no parent exists
container.append(task)
link[task.path] = task
return container
如果您想避免使用外键,您可以遍历查询集并在内存中重新创建树结构。
在我的例子中,我想要一个模板标签(很像 django-mptt's recursetree
templatetag) to show multiple levels of nested pages with only one database query. Basically copying mptt.utils.get_cached_trees
我最终得到了这个:
def get_cached_trees(queryset: QuerySet) -> list:
"""Return top-most pages / roots.
Each page will have its children stored in `_cached_children` attribute
and its parent in `_cached_parent`. This avoids having to query the database.
"""
top_nodes: list = []
path: list = []
for obj in queryset:
obj._cached_children = []
if obj.depth == queryset[0].depth:
add_top_node(obj, top_nodes, path)
else:
while not is_child_of(obj, parent := path[-1]):
path.pop()
add_child(parent, obj)
if obj.numchild:
path.append(obj)
return top_nodes
def add_top_node(obj: MP_Node, top_nodes: list, path: list) -> None:
top_nodes.append(obj)
path.clear()
def add_child(parent: MP_Node, obj: MP_Node) -> None:
obj._cached_parent = parent
parent._cached_children.append(obj)
def is_child_of(child: MP_Node, parent: MP_Node) -> bool:
"""Return whether `child` is a sub page of `parent` without database query.
`_get_children_path_interval` is an internal method of MP_Node.
"""
start, end = parent._get_children_path_interval(parent.path)
return start < child.path < end
可以这样使用它来避免可怕的 N+1 查询问题:
for page in get_cached_trees(queryset):
for child in page._cached_children:
...
我正在使用 django-treebeard
在 django-rest-framework
中开发具有分层数据结构的应用程序。我的(简化的)主要模型看起来像这样
class Task(MP_Node):
name = models.CharField(_('name'), max_length=64)
started = models.BooleanField(default=True)
我目前正在尝试实现的是所有根节点的列表视图,它显示额外的字段(例如是否所有子节点都已启动)。为此,我指定了一个视图:
class TaskViewSet(viewsets.ViewSet):
def retrieve(self, request, pk=None):
queryset = Task.get_tree().filter(depth=1, job__isnull=True)
operation = get_object_or_404(queryset, pk=pk)
serializer = TaskSerializer(operation)
return Response(serializer.data)
和序列化程序
class TaskSerializer(serializers.ModelSerializer):
are_children_started = serializers.SerializerMethodField()
def get_are_children_started(self, obj):
return all(task.started for task in Task.get_tree(obj))
一切正常,我得到了预期的结果。但是,我 运行 遇到了一个 N+1 查询问题,对于每个根任务,我需要分别获取所有子任务。通常这可以使用 prefetch_related
解决,但是当我使用 django-treebeard
中的物化路径结构时,任务模型之间没有 Django 关系,所以 prefetch_related
不知道该怎么做盒子。我试过使用自定义 Prefetch 对象,但由于这仍然需要 Django 关系路径,所以我无法让它工作。
我目前的想法是使用指向其根节点的外键扩展任务模型,如下所示:
root_node = models.ForeignKey('self', null=True,
related_name='descendant_tasks',
verbose_name=_('root task')
)
为了明确MP关系,方便查询。然而,这确实感觉有点不干脆,所以我想知道是否有人对如何解决它有其他建议。
最后我确实为每个指向其根节点的任务添加了一个外键,如下所示:
root_node = models.ForeignKey('self', null=True,
related_name='descendant_tasks',
verbose_name=_('root task')
)
我更新了任务模型上的保存方法,以确保我始终指向正确的根节点
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
try:
self.root_task = self.get_root()
except ObjectDoesNotExist:
self.root_task = None
return super(Task, self).save(force_insert=False, force_update=False, using=None,
update_fields=None
)
这让我可以使用 prefetch_related('descendants')
简单地预取所有后代。
每当我需要以嵌套方式拥有后代时,我都会使用以下函数再次嵌套扁平化的后代列表
def build_nested(tasks):
def get_basepath(path, depth):
return path[0:depth * Task.steplen]
container, link = [], {}
for task in sorted(tasks, key=attrgetter('depth')):
depth = int(len(task.path) / Task.steplen)
try:
parent_path = get_basepath(task.path, depth - 1)
parent_obj = link[parent_path]
if not hasattr(parent_obj, 'sub_tasks'):
parent_obj.sub_tasks = []
parent_obj.sub_tasks.append(task)
except KeyError: # Append it as root task if no parent exists
container.append(task)
link[task.path] = task
return container
如果您想避免使用外键,您可以遍历查询集并在内存中重新创建树结构。
在我的例子中,我想要一个模板标签(很像 django-mptt's recursetree
templatetag) to show multiple levels of nested pages with only one database query. Basically copying mptt.utils.get_cached_trees
我最终得到了这个:
def get_cached_trees(queryset: QuerySet) -> list:
"""Return top-most pages / roots.
Each page will have its children stored in `_cached_children` attribute
and its parent in `_cached_parent`. This avoids having to query the database.
"""
top_nodes: list = []
path: list = []
for obj in queryset:
obj._cached_children = []
if obj.depth == queryset[0].depth:
add_top_node(obj, top_nodes, path)
else:
while not is_child_of(obj, parent := path[-1]):
path.pop()
add_child(parent, obj)
if obj.numchild:
path.append(obj)
return top_nodes
def add_top_node(obj: MP_Node, top_nodes: list, path: list) -> None:
top_nodes.append(obj)
path.clear()
def add_child(parent: MP_Node, obj: MP_Node) -> None:
obj._cached_parent = parent
parent._cached_children.append(obj)
def is_child_of(child: MP_Node, parent: MP_Node) -> bool:
"""Return whether `child` is a sub page of `parent` without database query.
`_get_children_path_interval` is an internal method of MP_Node.
"""
start, end = parent._get_children_path_interval(parent.path)
return start < child.path < end
可以这样使用它来避免可怕的 N+1 查询问题:
for page in get_cached_trees(queryset):
for child in page._cached_children:
...