Django Admin在模型__init__缓存关联对象时触发多余查询的解决方法
解决Django Admin中Exam模型关联Pl的N+1查询问题
问题根源
你在Exam模型的__init__方法中直接访问self.pl缓存关联对象,但Django模型实例的初始化流程里,select_related预加载的关联数据是在父类__init__执行完成后,才填充到实例的_fields_cache中的。这就导致__init__里访问self.pl时,预加载数据还未到位,只能触发单独的数据库查询,进而产生N+1问题。
解决方案
1. 使用Django的cached_property延迟缓存关联对象
这是最简洁可靠的方案,利用Django内置的缓存属性工具,既实现缓存,又能自动复用select_related预加载的数据:
from django.db import models from django.utils.functional import cached_property class Pl(models.Model): name = models.CharField(max_length=100) class Exam(models.Model): pl = models.ForeignKey(Pl, on_delete=models.CASCADE) @cached_property def cached_pl(self): # 第一次访问时获取关联对象,后续直接调用缓存 return self.pl
在Admin列表中需要显示Pl相关字段时,直接调用cached_pl即可,比如配置list_display:
from django.contrib import admin class ExamAdmin(admin.ModelAdmin): list_display = ('id', 'get_pl_name') list_select_related = ('pl',) # 确保预加载关联数据 def get_pl_name(self, obj): return obj.cached_pl.name get_pl_name.short_description = 'Pl名称'
2. 确保Admin查询集正确应用select_related
如果之前的list_select_related或重写get_queryset没生效,检查以下细节:
- 重写
get_queryset时必须正确调用父类方法:
def get_queryset(self, request): qs = super().get_queryset(request) # 明确指定预加载pl,避免被其他逻辑覆盖 return qs.select_related('pl')
- 排查是否有自定义过滤器、搜索方法重新生成查询集,导致丢失
select_related配置。
3. 避免在__init__中提前访问关联对象
不要在模型__init__中直接访问关联对象,若必须在实例初始化后持有关联对象引用,可通过post_init信号实现:
from django.db.models.signals import post_init from django.dispatch import receiver @receiver(post_init, sender=Exam) def cache_pl_after_init(sender, instance, **kwargs): # post_init信号在实例完全初始化后触发,此时select_related数据已就绪 instance.cached_pl = instance.pl
这种方式效果和cached_property类似,但代码更繁琐,优先推荐前者。
验证方法
开启Django查询日志确认是否解决N+1问题:
# 在settings.py中添加 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], }, }, }
访问Admin列表页时,控制台只会输出1次查询(获取Exam列表并预加载Pl),而非101次(1次Exam+100次Pl)。
内容的提问来源于stack exchange,提问作者Anup
相关产品推荐
相关产品推荐

