prefetch_related()をちょっと詳しく調べてみた

はじめに

昨日はselect_related()についてちょっと詳しく調べてみました。その流れで今日はselect_related()のお友達のprefetch_related()についてちょっと詳しく調べてみます。余談ですが昨日やったselect_related()はさっそく今日仕事で使うタイミングが来て、ちょっと興奮しました。

ざっくりprefetch_related(*lookups)

QuerySet APIの中でもQuerySetを返すタイプのメソッドです。処理内容としては、一度のクエリでリレーション先のオブジェクトも取ってきます。でも、あれ、それだとselect_related()と一緒やないかい。と思いました。なんかprefetch_related()はM2Mのときに使うって聞いたけど、ざっくりすぎるので、何が違うのか適度にざっくり学びます。

Djangoドキュメントいわく、「select_related()prefetch_related()はどちらもリレーション先にアクセスすることで無駄にクエリを発行しないしないためのもの。という共通点はあるが、その処理内容は違うんだよ〜〜」とのこと。

select_related()のばあい

select_related()はリレーション先のオブジェクトも含めてSQLSELECTを実行してオブジェクトを取得している。そのためリレーション先のオブジェクトも含めて一度のクエリですべての対象オブジェクトを取得している。だがしかし!これだとM2Mの関係にあるオブジェクトを対象に実行した時、えらい数のオブジェクトを取得することになる。それはまずい!ということで、select_relatedForeignKeyone-to-oneのリレーションにだけ行うよう制限されてる。

prefetch_related()

一方でprefetch_relatedはというと、リレーション先を取得するために別のクエリを発行します。そして取得したオブジェクトとリレーション先をSQLではなく、Pythonを使ってJoiningします。だからprefetchなんですね。なるほど〜〜。

そしてこのメソッドはGenericRelationGenericForeignKeyもサポートしています。しかしselect_related()がサポートしてるForeignKeyone-to-oneには使えません。  

ちょっと詳しく

キャッシュがクリアされるパターン

Djangoドキュメントからコードをもってきました。このコードには問題があるそうです。どこでしょうか。

>>> pizzas = Pizza.objects.prefetch_related('toppings')
>>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

一見、ちゃんとprefetch_related()を使って無駄なクエリを排除しているように見えます。が、しかし実際は2行目のfor文でDBに何度もアクセスしてしまいます。なぜならQuerySetsの特徴で新たにチェーンメソッドを加えると違うクエリが発行されるからだそうです。ここではfilter()を使っているので新たにprefetchされていないpizzaオブジェクトがfor文で処理されてしまっています。気をつけねば。

意図的にキャッシュをクリアしたいパターン

select_related()と同じくキーワード引数にNoneを与える。 >>> non_prefetched = qs.prefetch_related(None)

Prefetch() オブジェクトを使うパターン

Prefetch(lookup, queryset=None, to_attr=None)

基本的な使い方

Prefetch()オブジェクトはprefetch_related()をコントロールするのに使います。lookupというキーワード引数はprefetch_related()lookupと同じです。ドキュメントのコードを貼ります。

>>> from django.db.models import Prefetch

>>> Question.objects.prefetch_related(Prefetch('choice_set')).get().choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

この様にリレーション先のオブジェクトを取得できます。

qeurysetを設定してみるパターン

続いてqueyset引数を設定してみたパターン。querysetを設定することでリレーション先として参照するオブジェクトを絞ることができます。フィルター的な役割を果たしています。

>>> voted_choices = Choice.objects.filter(votes__gt=0)
>>> voted_choices
<QuerySet [<Choice: The sky>]>
>>> prefetch = Prefetch('choice_set', queryset=voted_choices)
>>> Question.objects.prefetch_related(prefetch).get().choice_set.all()
<QuerySet [<Choice: The sky>]>

to_attrを設定してみるパターン

to_attr引数はprefetchに独自の属性を与えることができます。

>>> prefetch = Prefetch('choice_set', queryset=voted_choices, to_attr='voted_choices')
>>> Question.objects.prefetch_related(prefetch).get().voted_choices
<QuerySet [<Choice: The sky>]>
>>> Question.objects.prefetch_related(prefetch).get().choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

voted_choicesを設定することで、よりオブジェクトに対してどんな関係のリレーション先なのかが明確になりますね。そして、to_attrを使って取得したprefetchedの結果はリストに保存されるそうです。これは従来、prefetch_relatedがQuerySetインスタンスに結果をキャッシュしていたときよりも高速に処理ができるのでおすすめです。to_attrをうまく使いこなさねば。

おわりに

select_related()より手ごわかった……ですが、2日続けてselect_related()prefetch_related()をセットで学べて良かったです。頭に入りやすかった。Djangoはまだまだ奥深いですな。関係ないですがGoogleで検索してると、多少のパーソナライズはされてるにしてもたびたび知ってる人がでてきてなんか面白かったです。いろいろ参考にさせていただきました( )

参考リンク

QuerySet API reference | Django documentation | Django python - What's the difference between select_related and prefetch_related in Django ORM? - Stack Overflow djangoのSQL実行を最適化する(prefetch_related/select_related/Prefetch) - matobaの学んだこと Djangoでprefetch_relatedを使ってクエリ数を減らす - 偏った言語信者の垂れ流し