はじめに
昨日は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()
はリレーション先のオブジェクトも含めてSQLでSELECT
を実行してオブジェクトを取得している。そのためリレーション先のオブジェクトも含めて一度のクエリですべての対象オブジェクトを取得している。だがしかし!これだとM2Mの関係にあるオブジェクトを対象に実行した時、えらい数のオブジェクトを取得することになる。それはまずい!ということで、select_related
はForeignKey
かone-to-one
のリレーションにだけ行うよう制限されてる。
prefetch_related()
一方でprefetch_related
はというと、リレーション先を取得するために別のクエリを発行します。そして取得したオブジェクトとリレーション先をSQLではなく、Pythonを使ってJoiningします。だからprefetch
なんですね。なるほど〜〜。
そしてこのメソッドはGenericRelation
とGenericForeignKey
もサポートしています。しかしselect_related()
がサポートしてるForeignKey
やone-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を使ってクエリ数を減らす - 偏った言語信者の垂れ流し