使用getattr、ForeignKey与prefetch_related引发多SQL查询的Django问题
问题分析与解决方案
哈哈,这个问题我在Django 1.x版本里踩过坑!本质是老版本Django中select_related处理可空外键的特殊逻辑,结合你的场景给你拆解清楚:
背后的原因
先还原一下你的模型结构(应该是类似这样的):
from django.db import models class One(models.Model): title = models.CharField(max_length=100) class Two(models.Model): title = models.CharField(max_length=100) class MainModel(models.Model): # 非空外键关联One one = models.ForeignKey(One, on_delete=models.CASCADE) # 可空外键关联Two two = models.ForeignKey(Two, on_delete=models.CASCADE, null=True)
你的get_one()和get_two()应该是用了不带参数的select_related(),比如:
def get_one(): # select_related() 默认会自动预取所有非空的正向外键 mains = MainModel.objects.select_related().all() for main in mains: print(main.one.title) # 已经预取过,不会触发额外SQL def get_two(): mains = MainModel.objects.select_related().all() for main in mains: print(main.two.title) # two是可空外键,select_related()没预取,每个实例触发一次查询
在Django 1.9及更早版本中,select_related()默认只会跟随非空的正向外键,对于设置了null=True的可空外键,它不会自动包含到预取列表里。这就是为什么你访问two.title时会触发N+1查询。而当你删掉two的null=True,它变成非空外键,select_related()就会自动把它加入预取,行为就和one一致了。
解决办法
方案一(保留null=True):显式指定预取可空外键
修改get_two()的查询语句,把two明确加到select_related的参数里:
def get_two(): # 显式指定预取one和two mains = MainModel.objects.select_related('one', 'two').all() for main in mains: print(main.two.title) # 现在会一次性预取,无额外SQL
方案二(业务允许的话):移除two字段的null=True
这样select_related()会自动预取这个非空外键,不需要修改查询逻辑,行为就和one完全一致了。
版本差异补充
顺带提一句:这个行为在Django 1.10之后被优化了,select_related()默认会跟随所有正向外键(不管是否允许null),如果后续升级版本的话也能解决,但你用的是1.9.5,就先按上面的方案处理就行。
内容的提问来源于stack exchange,提问作者paralosreg
相关产品推荐
相关产品推荐

