如何解决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
相关产品推荐
相关产品推荐

