QuerySet APIに泣き、QuerySet APIに笑った1日

奥深いQuerySet APIの世界の入り口

CBVをうまく使えばViewsを簡潔に書けることがわかった。ほんとCBVはパワフル。「CBV最高だぜ〜〜ひゃっは〜〜」と調子に乗ってるとき、Viewsのなかでもう1つパワフルに暴れまわってくれる相棒を見つけた。QuerySet APIくんです。

例によって参考元はDjangoのドキュメント。

QuerySet APIをチェック

最近、Viewsのなかでは息をするようにデータベースをいじっていたので、QuerySetAPIをざっと見ておけばいざというとき頭の中のインデックスが反応してくれるんじゃないかと期待しています。Djangoのドキュメントにならってコード例はブログっぽい感じにしてみた。とりあえず、QuerySetを返すAPIだけ。

filter(**kwargs)

与えられたパラメータにマッチしたオブジェクトを返してくれる。

Entriy.objects.filter(tag="Django")

Djangoタグが付いた記事だけひっぱってくる。

exclued(**kwargs)

与えられたパラメータにマッチしなかったオブジェクトを返してくれる。

Entriy.objects.filter(tag="Django")

Djangoタグがついてないすべての記事をひっぱってくる。はぶりだ。

annotate(*args, **kwargs)

ちょっとトリッキーなやつ。コードをまず見てみよう。

from django.db.models import Count
e = Entry.objects.annotate(Count('tags'))
e[0].tags
e[0].tags__count
2

このオブジェクトのtagstags__countなんてメソッドは本来無い。けど、予めCountを読み込んでいるので利用可能になっている。Aggregation functions という関数群があって、Countもその中の1つ。これを使いこなすにはAggregation functions も学ぶ必要があるなぁ〜〜。でもうまく使えればコードを綺麗に保てるね!

order_by(*fields)

デフォルトだとモデルのmetaで順番を決められるらしいけど、order_byは呼び出すタイミングでその順番を変えることができる。

entries = Entry.objects.order_by('published_at')

昇順でソートして記事を取得。ちなみにorder_by('?')はランダムらしい。おもろい。でも、処理が重くなるから注意とのこと。

reverse()

クエリセットの要素の順番を逆にする。

……なんか手元でreverse()できなかった(´;ω;`)

Djangoのドキュメントから引っ張ってきました。活用すればこんなふうに最新の5個のクエリセットを取ってこられるらしい。

my_queryset.reverse()[:5]

なんで動かないの(´;ω;`)

distinct(*fields)

重複データを取り除いてくれるやつ。ふつう意図して重複を許さない限り重複があることじたいよくないと思う。あんまり使いみちが……と思ったけど、今日バリバリ使った。なんか2つのクエリセットを1つにしようとしてた記憶がある(遠い目)。

Entry.objects.order_by('title').distinct('title')

重複したタイトルは抹殺されました。

values(*fields, **expressions)

辞書型のクエリセットを返します。モデルインスタンスとしてよりイテレーブルなデータとして扱いたい時にどうぞらしいです。

Entry.objects.values().first()
{'id': 1,
 'author_id': 1,
 'title': '猫の手借りたい丸の冒険その一',
 'category_id': 1,
 'contents': 'その旅がいつ始まったかは、もう覚えていない。',
 'published_at': datetime.date(2018, 11, 2),
 'valid': True}

取得したいフィールドを指定することも可能。

Entry.objects.values('id', 'author_id').first()
{'id': 1,
'author_id': 1}

しかも引数に**expressionsがあるのでannotate()を併用することができるらしい。頭痛くなってきた。それと外部キーは参照先のidが代入されるらしい。ほかにもいろいろ注意書きがあったからここにはいつか戻ってくる気がする。

values_list()

今度はtupleでクエリセットを返してくれるやつです。

Entry.objects.values_list().first()
(1, 1,  '猫の手借りたい丸の冒険その一', 1, 'その旅がいつ始まったかは、もう覚えていない。', datetime.date(2018, 11, 2), True)

フィールドを指定するとそのフィールドだけをtupleでかっぱらってきてくれる。記事の投稿日を指定してみた。

[(datetime.date(2018, 11, 2),), (datetime.date(2018, 11, 2),), (datetime.date(2018, 11, 2),)]

「え、tupleいらないんだけど」ってときは.values_list('published_at', flat=True) という感じでflatオプションを使う。

[datetime.date(2018, 11, 2), datetime.date(2018, 11, 2), datetime.date(2018, 11, 2)]

計画通り(にちゃあ

dates(field, kind, order='ASC')

日付を扱います。以下のようなデータがDBに入ってるとします。

datetime.date(2018, 1, 1)
datetime.date(2018, 2, 1)
datetime.date(2018, 2, 2)
datetime.date(2019, 2, 2)

Entry.objects.dates('published_at, 'year')

とすれば重複しないyearが取り出されます。つまりdatetime.date(2018, 1, 1)datetime.date(2019, 2, 2)を取得されます。

Entry.objects.dates('published_at, 'month')とすると今度は以下の2つが取得されます。

datetime.date(2018, 1, 1)
datetime.date(2018, 2, 1)

こんな要領で使えるやつらしいです。順番は基本昇順ですが、引数にDESCを加えれば降順にもできるらしい。

datetimes(field_name, kind, order="ASC", tzinfo=None)

datetime版です。tzinfo引数でタイムゾーンを設定できます。

none()

何のオブジェクトも返さないクエリセットを生成するらしい。哲学かな。

使いみちがいまいち見えなかったのでGitHubで実態を調べてみた。objects.none() filename:views.pyで5Kくらいヒットした。だらっと見かけた実用例。

  • ログイン認証で失敗したとき、return Hoge.objects.none()
  • results = Hoge.objects.none() で最初に初期化
  • 例外処理でreturn Hoge.objects.none()

ここらへんが多かった。なるほど。

all()

全員集合!(すべてのオブジェクトを取得)

union(*other_qs, all=False)

クエリセットを合体する。重複したデータは覗かれて、一意なオブジェクトのクエリセットができる。all=Trueを引数に設定すると重複もすべて合体する。

intersection(*other_qs)

複数のクエリセットが保持している同じオブジェクトだけを抽出する。

qs_a.intersection(qs_b)

difference(*other_qs)

qs_a.intersection(qs_b)

複数のクエリでqs_aqs_bの差分のオブジェクトを抽出する。

select_related(*fields)

外部キーのオブジェクトを参照することができる。

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

# またDBを叩く
b = e.blog

こういう処理をいっぺんに済ませる。

e = Entry.objects.select_related('blog').get(id=5)
# e.blogは予め呼び出されているのでDBにアクセスしないでいい
b = e.blog

実践的なテクニックだ。

prefetch_related(*lookups)

これはselect_relatedに少し似てるメソッドみたい。どちらもDBに関連するオブジェクトを取りに行くアクセスを減らすためにある。

select_relatedSQLSELECTステートメントで、関連するオブジェクトを1つのクエリのなかで取得する。しかし、M2Mで多くのオブジェクトと関係を持つものをselect_relatedで取得するのは避けたほうがいいみたい。select_relatedは1対1の関係のオブジェクトにのみ使うものだそうだ。

一方prefetch_relatedはそれぞれのオブジェクトを個別に取得してそのあとにjoining しているらしい。この処理の違いからprefetch_related()はM2Mの外部キーのオブジェクトにも対応している。使い分けである。

extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)

引数の種類がおおい。よくみるとSQLのコマンドばかりだ。これはDjangoのクエリ操作だけじゃ解読困難に陥るようなコードをSQLコマンドで書くもの。以下の2つは等価。

qs.annotate(val=RawSQL("select col from sometable where othercol = %s", (someparam,)))
qs.extra(
     select={'val': "select col from sometable where othercol = %s"},
     select_params=(someparam,),
)

defer(*fields)

不要なフィールドはとってこないで!とお願いできるメソッド。たとえばでっかいデータを保持するcontentフィールドがあったとして、10,000件のオブジェクトをとってこようとすると処理が遅くなる。ので、defer()contentはいらないよ!と伝えようということ。

しかし、取得したオブジェクトのcontentフィールドにアクセスするとそのタイミングでDBにアクセスしてデータを取得してきてくれるらしい。本当にとってきてないのかぁ〜〜シュレディンガーのにゃんにゃんみたいだ。

only(*fields)

逆にほとんどのフィールドをとってこなくていいときはonly()で必要なものだけとってくる。

using(alias)

複数のDBを使っている時、使用するDBを指定する。

Entry.objects.using('backup')

select_for_update(nowait=False, skip_locked=False)

ちょっとこれだけよくわからなかった。rowをロックしてほかの処理をうんちゃらかんちゃら。SQLの基礎知識が/(^o^)\

QuerySet API reference | Django documentation | Django

raw(raw_query, params=None, translations=None)

SQLクエリを実行できる。返り値の型はdjango.db.models.query.RawQuerySet

おわりに

知らない権利は無いのと一緒のように知らないメソッドも無いのと一緒だと痛感した1日だったので、鉄は熱いうちに打てという気持ちでざっと見てみました。

参考リンク

QuerySet API reference | Django documentation | Django