select_related()の仕様を理解する

はじめに

最近、QuerySet APIについてよく調べていました。今日はQuerySet APのメソッドのなかでもドキュメントの文量が多めなselect_related() をすこし詳しく学習します。なにかと利用する機会がありそう。(あと923がselect_related()は奥が深いぞ的なことを言っていたので)

ざっくりした理解

select_related(*fields)

クエリを実行したときに、指定された外部キーのオブジェクトも一緒にとってくる。というやつ。それによって、DBを叩く回数を節約できる。

# DBを叩く
e = Entry.objects.get(id=5)

# またDBを叩いてる
b = e.blog

こういうコードを以下の様に書ける。

# DBを叩く
e = Entry.objects.select_related('blog').get(id=5)

# 予め取得したオブジェクトを参照しているのでDBを叩いてない
b = e.blog

もうちょっと詳しく

迂闊な経験

一度や二度のアクセスならまだしも、たくさんのオブジェクトをfor文で回して外部キーを参照するとすごい沢山の回数DBを叩くことになる。ぼくこれは経験があります。もうあんな目に遭うのはいやです。そのためにももう少しselect_related()について詳しく学んで今後に備えます。

外部キーの外部キーを取ってくるパターン。

ここに街と著者と記事のモデルがあります。

from django.db import models

class City(models.Model):
    # ...
    pass

class Author(models.Model):
    # ...
    hometown = models.ForeignKey(
        City,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

class Entry(models.Model):
    # ...
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

そして記事の著者の故郷を知りたいとします。まずはselect_relatedを使用しないパターン。

b = Entry.objects.get(id=4)  # DBを叩く
p = b.author         # DBを叩く
c = p.hometown       # DBを叩く

愚直に行えばこのように計3回叩くことになることになります。続いてselect_relatedを使うパターン。

# 一度のクエリでauthorとhometownテーブルからもオブジェクトを取得する
b = Entry.objects.select_related('author__hometown').get(id=4)
p = b.author         # 取得済みのオブジェクトを参照
c = p.hometown       #取得済みのオブジェクトを参照

DBを叩きにいく回数が1回で済みました。やったね!

リレーション先のリストをクリアする

select_relatedで取得したリレーション先のオブジェクトをクエリセットからクリアしたい時がいつか来るかもしれません。これをするにはキーワード引数にNoneを与えて実行します。

without_relations = queryset.select_related(None)

さよならリレーション。

複数のselect_related()をチェーンするパターン

これは select_related('foo').select_related('bar').

こう書けます select_related('foo', 'bar')

頭の片隅に入れておきます。

リレーション先のオブジェクトが存在しないパターン

どうなるか見てみましょう。

# ブログ記事の作者を確認
entry.author
# 作者はゴリラでした
<Author: ゴリラ>
entry.author.pk
1
# ゴリラを削除
Author.objects.get(pk=1).delete()
entry = Entry.objects.select_related('author').first()
entry
<Entry: ゴリラです。ブログ始めました。>
entry.author == None
True

ゴリラオブジェクトを削除しても平気な顔してQuerySetを用意してきました。しれっとNoneが入ってます。エラーは帰ってきません。これを忘れるといつか痛い目に遭うと思うので覚えておきます。

引数を指定しないパターン

引数を指定しないとnull=Falseの外部キーのみが取得対象になる。基本は引数を与えるべし。明示的にいこう。

リレーション先のデータを取得したいパターン

とりあえずリレーション先のデータがほしいときは__(アンダースコア2つ)を使った表現を使う。ゴリラオブジェクトのnamedescriptionを取得してみます。

entry = Entry.objects.filter(pk=1).select_related(
     'author'
).values('author__name','author__description')
<AuthorQuerySet [{'author__name': 'ゴリラ', 'author__description': 'ワイルドだぜぇ。'}]>

取得できました。これも意外と使い所があるかも。

おわりに

これでもうselect_relatedを恐れること無く使えます。

参考リンク

QuerySet API reference | Django documentation | Django

Django ORM の select_related, prefetch_related の挙動を詳しく調べてみた - akiyoko blog

Djangoのselect_relatedとN+1問題 - Misc Notes

django - How to use select_related - Stack Overflow