在前面教程中小編我已經介紹了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方法額外添加額外條件和屬性。