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

如何解决Django ModelAdmin中M2M与filter_horizontal的延迟问题?

解决Django ModelAdmin中十万级M2M字段filter_horizontal加载延迟问题

在GroupMailIds的ModelAdmin中,通过formfield_for_manytomany自定义了mailboxes字段的查询集过滤逻辑,但当查询集规模达到十万级时,filter_horizontal组件加载耗时过长甚至超时。要求仅使用原生filter_horizontal,不依赖第三方库(如django-autocomplete-light)或额外JavaScript,且已完成查询集的业务过滤。

1. 移除冗余查询操作,优化基础查询性能

你的代码中存在两处冗余操作,直接拖慢查询效率:

  • prefetch_related('domain'):由于使用了only('id','email'),查询仅返回这两个字段,domain相关数据不会被用到,该预查询完全多余,会额外增加数据库查询开销。
  • 重复设置queryset:先给kwargs["queryset"]赋值,之后又设置field.widget.choices.queryset,导致查询集被评估两次,浪费资源。

修改后的formfield_for_manytomany代码:

def formfield_for_manytomany(self, db_field, request, **kwargs):
    if db_field.name == "mailboxes":
        if request.user.is_superuser:
            # 移除不必要的prefetch_related
            queryset = Mailbox.objects.only('id', 'email')
            kwargs["queryset"] = queryset
            return super().formfield_for_manytomany(db_field, request, **kwargs)

        if request.user.groups.filter(name__in=['customers']).exists():
            queryset = Mailbox.objects.filter(
                domain__customer__email=request.user.email
            ).only('id', 'email')  # 移除prefetch_related
            kwargs["queryset"] = queryset
            return super().formfield_for_manytomany(db_field, request, **kwargs)
    return super().formfield_for_manytomany(db_field, request, **kwargs)

2. 优化数据库索引,加速过滤查询

确保过滤条件涉及的字段都有数据库索引,避免全表扫描:

  • 确认Domain模型的customer外键字段已默认创建索引(Django ForeignKey默认添加索引)。
  • 若Customer模型的email字段未设置db_index=True,添加该属性:
    class Customer(models.Model):
        email = models.EmailField(db_index=True)
        # 其他字段...
    
  • Mailbox的email字段已设置unique=True,自动包含索引,无需额外操作。

3. 自定义迭代器减少内存占用,优化HTML渲染

十万级数据的核心问题之一是:原生FilteredSelectMultiple会一次性将所有对象加载到内存并渲染为<option>标签,导致内存溢出和浏览器解析超时。通过自定义ModelChoiceIterator,使用iterator()方法分批从数据库读取数据,避免一次性加载所有对象。

步骤1:定义优化的迭代器和Widget

from django.contrib.admin.widgets import FilteredSelectMultiple, ModelChoiceIterator

class OptimizedModelChoiceIterator(ModelChoiceIterator):
    def __iter__(self):
        if self.field.empty_label is not None:
            yield ("", self.field.empty_label)
        # 使用iterator()逐行读取数据库数据,不一次性加载所有对象到内存
        for obj in self.queryset.iterator():
            yield self.choice(obj)

class OptimizedFilteredSelectMultiple(FilteredSelectMultiple):
    @property
    def choices(self):
        if hasattr(self, '_choices'):
            return self._choices
        return OptimizedModelChoiceIterator(self)
    
    @choices.setter
    def choices(self, value):
        self._choices = value

步骤2:在ModelAdmin中使用自定义Widget

修改formfield_for_manytomany,替换默认Widget:

def formfield_for_manytomany(self, db_field, request, **kwargs):
    if db_field.name == "mailboxes":
        if request.user.is_superuser:
            queryset = Mailbox.objects.only('id', 'email')
            kwargs["queryset"] = queryset
            field = super().formfield_for_manytomany(db_field, request, **kwargs)
            field.widget = OptimizedFilteredSelectMultiple(field.widget.verbose_name, field.widget.is_stacked)
            return field

        if request.user.groups.filter(name__in=['customers']).exists():
            queryset = Mailbox.objects.filter(
                domain__customer__email=request.user.email
            ).only('id', 'email')
            kwargs["queryset"] = queryset
            field = super().formfield_for_manytomany(db_field, request, **kwargs)
            field.widget = OptimizedFilteredSelectMultiple(field.widget.verbose_name, field.widget.is_stacked)
            return field
    return super().formfield_for_manytomany(db_field, request, **kwargs)

4. 可选:限制默认显示的未选中选项(仅后端控制)

如果业务允许,可以默认只显示少量未选中选项(如最近创建的100条),用户需要更多选项时通过filter_horizontal的搜索框触发精准查询(注意:原生搜索是客户端过滤,若要服务器端搜索需JS,此处仅做后端默认限制):

# 以超级用户为例,默认仅显示最近创建的100个Mailbox
queryset = Mailbox.objects.only('id', 'email').order_by('-id')[:100]

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 06:54:53