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

Django一对多模型嵌套循环查询优化与通用函数实现

问题描述

我有两个Django模型(PassDown与Entry),是一对多关系:一个PassDown对应多个Entry。目前我通过嵌套循环查询生成父子对象字典,但数据库查询次数过多。尝试用select_related()和prefetch_related()优化反向关联查询却没成功;同时希望将查询函数通用化,但反向关联的xxx_set(比如这里的entry_set)需要模型名小写,没法直接通过参数传递模型类实现,想问有没有内置方法解决,不用修改模型名称。

相关代码如下:

视图代码

class MasterListView(ListView):
    template_name = 'passdown/master_list.html'
    context_object_name = 'entries'

    def nested_query_loop(self):
        object_list = []
        all_objects = PassDown.objects.all() #queryset containing all passdown objects
        for object in all_objects:
            object_list.append(object.pk) #list containing all passdown primary keys (object_list)

        query_dict={}
        for i in object_list:
            temp_obj = PassDown.objects.get(pk=i) #ith passdown object itself
            sub_object_pk_list=[]
            all_i_entries = temp_obj.entry_set.all() #queryset containing all entry objects of i
            for j in all_i_entries:
                sub_object_pk_list.append(j.pk) #list containing all entry primary keys of i (sub_object_pk_list)
                
            sub_object_list = []
            for k in sub_object_pk_list:
                sub_object_list.append(all_i_entries.get(pk=k)) #list containing all ith entry objects (sub_object_list)
            query_dict.__setitem__(temp_obj, sub_object_list)

        return query_dict

    def get_queryset(self):
        queryset = PassDown.objects.all()
        return queryset
    
    def get_context_data(self, **kwargs):
        context = super(MasterListView, self).get_context_data(**kwargs)
        queryset = self.nested_query_loop()
        context.update({
            "parent_child_dict": queryset
        })
        return context

模型代码

class PassDown(models.Model):
    shiftList = [("Days", "Days"),
                 ("Nights", "Nights"),
                 ("Mids", "Mids")]
    shift = models.CharField(max_length=10, choices=shiftList)
    date_time = models.DateTimeField(default=timezone.now)
    notes = models.TextField()
    entered_by = models.ForeignKey(User, on_delete=models.DO_NOTHING)
    #entry = models.ForeignKey(Entry, on_delete=models.DO_NOTHING)

    def __str__(self):
        return f"{self.shift} {self.date_time}"

class Entry(models.Model):
    modex = models.IntegerField()
    discrepancy = models.CharField(max_length=50)
    text_body = models.TextField()
    passdown = models.ForeignKey(PassDown, on_delete=models.CASCADE, default=PassDown.objects.last)

    def __str__(self):
        return f"{self.modex} {self.discrepancy} {self.passdown.date_time}"

解决方案

1. 优化数据库查询(解决N+1问题)

你的nested_query_loop存在大量冗余查询:先遍历所有PassDown取PK,再循环get每个PassDown,重复查询entry_set并二次获取子对象,这会触发N+M次数据库查询(N是PassDown数量,M是所有Entry总数)。

用prefetch_related可一次性预取所有反向关联的Entry对象,仅需2次查询(一次查所有PassDown,一次查所有关联的Entry,Django会自动完成关联)。优化后的视图代码如下:

class MasterListView(ListView):
    template_name = 'passdown/master_list.html'
    context_object_name = 'entries'

    def get_queryset(self):
        # 预取所有关联的entry对象,彻底解决N+1查询问题
        return PassDown.objects.prefetch_related('entry_set')
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # 直接遍历预取后的queryset生成字典,无需额外查询
        parent_child_dict = {passdown: list(passdown.entry_set.all()) for passdown in self.get_queryset()}
        context.update({"parent_child_dict": parent_child_dict})
        return context

为什么之前prefetch_related没生效?

大概率是你没有在初始查询集上调用它,或是后续重新查询了对象(比如代码中用PassDown.objects.get(pk=i)重新获取对象,导致预取的缓存失效)。只要在初始的PassDown查询集上调用prefetch_related('entry_set'),后续访问passdown.entry_set会直接使用缓存,不会触发新查询。

2. 通用化查询函数(动态获取反向关联名称)

要实现通用化,无需硬写entry_set,可通过Django模型元数据动态获取反向关联的访问器名称,核心方法是子模型._meta.get_field('外键字段名').remote_field.get_accessor_name()。

通用化函数实现

def get_parent_child_dict(parent_model, child_model, child_foreign_key_field):
    # 动态获取反向关联的访问器名称(比如默认的entry_set)
    accessor_name = child_model._meta.get_field(child_foreign_key_field).remote_field.get_accessor_name()
    # 预取所有关联子对象
    parent_queryset = parent_model.objects.prefetch_related(accessor_name)
    # 生成父子字典
    return {parent: list(getattr(parent, accessor_name).all()) for parent in parent_queryset}

在视图中调用通用函数

class MasterListView(ListView):
    template_name = 'passdown/master_list.html'
    context_object_name = 'entries'

    def get_queryset(self):
        return PassDown.objects.all()
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        # 调用通用函数:传入父模型、子模型、子模型中关联父模型的外键字段名
        parent_child_dict = get_parent_child_dict(PassDown, Entry, 'passdown')
        context.update({"parent_child_dict": parent_child_dict})
        return context

原理说明

  • child_model._meta.get_field(child_foreign_key_field):获取子模型中关联父模型的外键字段对象。
  • remote_field.get_accessor_name():获取该外键对应的反向关联访问器名称,默认是xxx_set(xxx为子模型名小写);如果外键字段设置了related_name,该方法会返回你自定义的名称,完全无需硬编码。

这样无论模型名称如何修改,只要传递正确的模型类和外键字段名,就能动态生成父子字典,实现完全通用。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 08:27:08