在前面教程中小编我已经介绍了Django的Queryset特性及高级使用技巧,今天我们再来学习两个非常重要的查询方法select_related和prefetch_related方法,看看如何使用它们避免不必要的数据库查询。高手过招,只差分毫。专业和业余之前的区别就在细节的处理上。为了让大家更直观地看到这两个方法的作用,我们将安装使用django-debug-toolbar这个流行的Django第三方包。
django-debug-toolbar的安装
第一步:pip install django-debug-toolbar
第二步:打开项目文件夹settings.py 文件, 把"debug_toolbar"加到INSTALLED_APP里去。
第三步: 打开项目文件夹里的urls.py, 把debug_toolbar的urls加进去。
from django.conf import settings from django.conf.urls import include, url # For django versions before 2.0 from django.urls import include, path # For django versions from 2.0 and up if settings.DEBUG: import debug_toolbar urlpatterns = [ path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns
第四步: 在settings.py里添加中间件
MIDDLEWARE = [ # ... 'debug_toolbar.middleware.DebugToolbarMiddleware', # ... ]
第五步: 在settings.py设置本地IP, debug_toolbar只能在localhost本地测试环境下运行。
INTERNAL_IPS = [ # ... '127.0.0.1', # ... ]
当你安装好debug_toolbar后,启动django服务器,打开任何一个页面你都可以看到查询数据库所花时间以及是否有相似及重复的查询,如下图所示:
言归正传
假设我们有如下一个文章(Article)模型,其与类别(Category)是单对多地关系(ForeignKey), 与标签(Tag)是多对多的关系(ManyToMany)。我们需要编写一个article_list的函数视图,以列表形式显示文章清单及每篇文章的类别和标签,我们常规做法如下:
#models.py
class Article(models.Model): """文章模型""" title = models.CharField('标题', max_length=200, db_index=True) category = models.ForeignKey('Category', verbose_name='分类', on_delete=models.CASCADE, blank=False, null=False) tags = models.ManyToManyField('Tag', verbose_name='标签集合', blank=True)
#views.py
def article_list(request): articles = Article.objects.all() return render(request, 'blog/article_list.html', {'articles': articles, })
我们的模板代码会如下所示:
#blog/article_list.html
<ul> {% for article in articles %} <li>{{ article.title }} </li> <li>{{ article.category.name }}</li> <li> {% for tag in article.tags.all %} {{ tag.name }}, {% endfor %} </li> {% endfor %} </ul>
上面代码运行是没有错的,只是效率很低。使用debug_toolbar可以让我们更深入地看问题。它提示我们查询了10次数据库,包括3次重复查询,一共耗时8.93ms。
什么?显示一个页面竟用了10次查询?是的,你没看错。我们先分析下这会什么会发生,然后再解释如何使用select_related和prefetch_related方法解决这个问题。
为什么会有重复查询?
当我们使用Article.objects.all()查询文章时,我们做了第一次数据库查询,查询的是blog_article数据表, 得到的数据只是文章对象列表,然而并没有包含与每篇文章相关联的category和tags对象信息。当我们在模板中调用{{ article.category.name }} 和 {{ tag.name }}显示category和tags的名字时,Django还需要重新查询blog_category和blog_tag数据表获取名字。for循环每运行一次,django都要对数据库进行一次查询,造成了极大的资源浪费。为什么我们不能再第一次获取文章列表的同时就获取每篇文章相关联的category和tags对象信息呢?Django考虑到了这一点,所以提供select_related和prefetch_related方法来提升数据库查询效率,类似于SQL的JOIN方法。
select_related方法
select_related将会根据外键关系(注意: 仅限单对单和单对多关系),在执行查询语句的时候通过创建一条包含SQL inner join操作的SELECT语句来一次性获得主对象及相关对象的信息。现在我们对article_list视图函数稍微进行修改,加入select_related方法,在查询文章列表时同时一次性获取相关联的category对象信息,这样在模板中调用 {{ article.category.name }}时就不用再查询数据库了。
def article_list(request): articles = Article.objects.all().select_related('category') return render(request, 'blog/article_list.html', {'articles': articles, })
运行结果如下图所示,查询次数由10次变为了7次,时间降到了2.99ms。
selected_related常用使用案例如下:
# 获取id=13的文章对象同时,获取其相关category信息 Article.objects.select_related('category').get(id=13) # 获取id=13的文章对象同时,获取其相关作者名字信息 Article.objects.select_related('author__name').get(id=13) # 获取id=13的文章对象同时,获取其相关category和相关作者名字信息。下面方法等同。 Article.objects.select_related('category', 'author__name').get(id=13) Article.objects.select_related('category').select_related('author__name').get(id=13) # 使用select_related()可返回所有相关主键信息。all()非必需。 Article.objects.all().select_related() # 获取Article信息同时获取blog信息。filter方法和selected_related方法顺序不重要。 Article.objects.filter(pub_date__gt=timezone.now()).select_related('blog') Article.objects.select_related('blog').filter(pub_date__gt=timezone.now())
prefetch_related方法
对于多对多字段,你不能使用select_related方法,这样做是为了避免对多对多字段执行JOIN操作从而造成最后的表非常大。Django提供了prefect_related方法来解决这个问题。prefect_related可用于多对多关系字段,也可用于反向外键关系(related_name)。我们对之前的article_list视图函数再做进一步修改,在查询文章列表的同时返回相关tags信息。
def article_list(request): articles = Article.objects.all().select_related('category').prefecth_related('tags') return render(request, 'blog/article_list.html', {'articles': articles, })
运行结果如下。查询次数减少到5次,运行时间1ms,是不是很帅?
prefetch_related使用方法如下:
# 文章列表及每篇文章的tags对象名字信息 Article.objects.all().prefetch_related('tags__name') # 获取id=13的文章对象同时,获取其相关tags信息 Article.objects.prefetch_related('tags').get(id=13)
现在问题来了,如果我们获取tags对象时只希望获取以字母P开头的tag对象怎么办呢?我们可以使用Prefetch方法给prefect_related方法添加条件和属性。
# 获取文章列表及每篇文章相关的名字以P开头的tags对象信息
Article.objects.all().prefetch_related( Prefetch('tags', queryset=Tag.objects.filter(name__startswith="P")) )
# 文章列表及每篇文章的名字以P开头的tags对象信息, 放在article_p_tag列表
Article.objects.all().prefetch_related( Prefetch('tags', queryset=Tag.objects.filter(name__startswith="P")), to_attr='article_p_tag' )
小结
当你查询单个主对象或主对象列表并需要在模板或其它地方中使用到每个对象的关联对象信息时,请一定记住使用select_related和prefetch_related一次性获取所有对象信息,从而提升数据库查询效率,避免重复查询。如果不确定是否有重复查询,可使用django-debug-toolbar查看。
对与单对单或单对多外键ForeignKey字段,使用select_related方法
对于多对多字段和反向外键关系,使用prefetch_related方法
两种方法均支持双下划线指定需要查询的关联对象的字段名
使用Prefetch方法可以给prefetch_related方法额外添加额外条件和属性。