这两个方法实在是太常见了,尤其是select_related已经在业务代码中使用过很多次,但是这次需要来对这两个在很多方面相似的函数在使用层面上做一个彻底的分析来明确它们的区别
如果非要从返回的结果这个视角来看这两者的作用,那么其实一句话就可以概括了:select_related的功能是prefetch_related的子集,任何可以用select_related实现的逻辑,prefetch_related都可以同样实现
然而如果真的现实中的情况如上面这句话一般,那django设计select_related这个方法就完全是多余的了,这明显是不可能的,所以下面来具体讲一下它们的区别
假设我们的项目中有如下模型
class Messages(CommonInfo):
record_type = models.ForeignKey('ObjectRecordTypes', models.DO_NOTHING)
product = models.ForeignKey(Products, blank=True, null=True)
content = models.TextField(max_length=500, blank=True, null=True)
description = models.TextField(max_length=500, blank=True, null=True)
display_order = models.IntegerField(3)
owner = models.ForeignKey(User, models.DO_NOTHING, db_column='owner',
blank=True, null=True)
external_id = models.CharField(max_length=255, default=None, blank=True,
null=True)
class Meta:
db_table = 'messages'
上面这个message模型有一个product外键关联向Products类,而根据django orm的foreign key定义,这个外键指的就是可以有多条message对象同时指向一条product对象,也就是典型的多对一,另外因为这里的product字段在定义时属性中并没有对related_name这个属性进行设置,所以这里是可以通过product对象反向找到外键关联它的所有message的
好了,准备工作完成,再看下具体使用select_related和prefetch_related是什么情况吧
>>> models.Products.advance_objects.select_related('messages_set').filter(id__lt=100)
print result
>>> FieldError: Invalid field name(s) given in select_related: 'messages_set'.
>>> models.Products.advance_objects.prefetch_related('messages_set').filter(id__lt=100)
>>> print result
>>> (正常显示结果)
上面这个例子在某种程度上更加清晰的解释了django文档中对这两个方法的不同的描述:select_related是在关联的对象确定只有一个的情况下才能使用,prefetch_related则不管关联的对象有几个,全部可以使用,因为它取的是一个集合
不同的地方说完了,再来说下产生结果相同的一种情况
>>> result = models.EventExpenses.advance_objects.select_related('event').get(pk=1)
>>> 取到一个会议费用的对象,同时将费用外键关联的会议对象的信息也取到了
>>> result = models.EventExpenses.advance_objects.prefetch_related('event').get(pk=1)
>>> 取到一个会议费用的对象,同时将费用外键关联的会议对象的信息也取到了
从上面看起来,两个操作拿到的结果是一样的,那区别在哪里呢,我们仔细看一下orm打印的日志,区别就很明显了,首先对于select_related,后台只执行了一条SQL语句
SELECT `event_expenses`.xxx, `events`.xxx
FROM `event_expenses`
INNER JOIN `events`
ON (`event_expenses`.`event_id` = `events`.`id`)
WHERE xxx=xxx;
显而易见,这里select_related采用了join内连接方式将两张表关联起来,所以只用了一条SQL语句
再看prefetch_related
SELECT `event_expenses`.xxx FROM `event_expenses` WHERE xxx=xxx;
SELECT `events`.xxx FROM `events` WHERE `events`.`id` IN (xxx, xxx);
这里prefetch执行了两条语句来拿到外键关联的数据,同时第二条数据采用的SQL语句是用了IN谓语的sql,这样再看就能发现,虽然它们取到的结果是一样的,但是prefetch_related操作明显更费时,不仅是因为它执行了两条SQL语句,还因为第二条SQL语句使用了SQL中很费时的IN谓语来进行判断
通过上面的例子再看,实际上也可以很明显的知道这两个方法各自的应用场景了:在关联的外键可能指向多个数据对象时,prefetch_related是唯一选择,而在外键指向的是一个唯一对象时,select_related方法明显要比prefetch_related高效,虽然prefetch_related也能用
今天在项目代码review的过程中老司机提了一句select_related的用法问题,假设有一个Blog,有几篇Article关联到这个blog对象,如果我们通过其中一篇article实例找关联的blog的名字,我们很可能下意识的写出下面的语句
Article.objects.select_related('blog__name').filter(xxx=xxx)
但是上面的情况经过试验,实际上是有问题的,这样去用select_related实际上还是会取到blog所有字段的值,同时在某些情况下这种blog__name的queryset还会报错,因为name字段根本不是外键,所以这里明确了一个事情即:select_related方法里的字段都应该是外键关联字段,而不是普通字段