こんにちは、かつコーチです。
前回はForeignKey・ManyToManyでモデル同士のリレーションを組む方法を解説しました。
今回は、そのモデルからデータを取り出すときに必ず使う「QuerySet」の基本を扱います。
SQLを書かずにデータベースを操作できるDjangoのORM(Object-Relational Mapping、Pythonのオブジェクトとデータベースをつなぐ仕組み)の中心となる機能なので、しっかり押さえておきましょう。
QuerySetとは?
QuerySetの定義
QuerySetとは、データベースに問い合わせた結果を表すオブジェクトのことです。
Article.objects.all()のように書くと、Articleテーブルの全レコードを表すQuerySetが返ってきます。
from myapp.models import Article
articles = Article.objects.all()
print(articles)
# <QuerySet [<Article: 1本目の記事>, <Article: 2本目の記事>]>
見た目はリストのようですが、実体は「まだ実行されていないSQLクエリの予約」に近いものです。
遅延評価という仕組み
QuerySetの大事な性質が遅延評価(実際に必要になるまでSQLを実行しない仕組み)です。
articles = Article.objects.filter(category__name="Django") # この時点ではSQLは実行されない
for article in articles: # ここで初めてSQLが実行される
print(article.title)
filter()を呼んだ時点ではSQLは発行されず、for文でループしたりリストに変換したりして初めてデータベースへ問い合わせが行われます。
この仕組みのおかげで、複数の条件を.filter()でつなげてから最後にまとめて実行する、といった書き方ができます。
フィルタの基本の書き方
filter()とexclude()
条件に一致するデータを取得するにはfilter()、一致しないデータを取得するにはexclude()を使います。
# category_idが1の記事を取得
Article.objects.filter(category_id=1)
# category_idが1以外の記事を取得
Article.objects.exclude(category_id=1)
# 複数条件をAND条件でつなげる
Article.objects.filter(category_id=1, is_published=True)
filter()に複数の引数を渡すと、自動的にAND条件になります。
フィールドルックアップで条件を細かく指定する
「〜以上」「〜を含む」といった細かい条件は、フィールド名の後に__(アンダースコア2つ)でルックアップを指定します。
from django.utils import timezone
# タイトルに「Django」を含む記事(大文字小文字を区別しない)
Article.objects.filter(title__icontains="Django")
# 公開日が今日以降の記事
Article.objects.filter(published_at__gte=timezone.now())
# カテゴリ名が「入門」の記事(リレーション先のフィールドを参照)
Article.objects.filter(category__name="入門")
よく使うフィールドルックアップは以下の通りです。
| ルックアップ | 意味 |
|---|---|
__gte __lte | 〜以上・〜以下 |
__gt __lt | 〜より大きい・〜より小さい |
__contains __icontains | 〜を含む(大文字小文字区別あり/なし) |
__in | リスト内のいずれかに一致 |
__isnull | NULLかどうか |
category__nameのように__でリレーション先のフィールドまでたどれるのも、QuerySetの大きな強みです。
取得件数を絞る・並び替える
get()とfirst()の違い
1件だけ取得したいときはget()かfirst()を使いますが、挙動が異なります。
# get():条件に一致するレコードが1件でなければ例外が発生する
article = Article.objects.get(pk=1)
# first():条件に一致する最初の1件を取得(0件ならNoneが返る)
article = Article.objects.filter(category_id=1).first()
get()は該当が0件でも複数件でも例外を投げるため、「必ず1件だけ存在する」と分かっている場合(主キー検索など)に向いています。
first()は0件でもNoneが返るだけなので、存在するかどうか分からない検索に向いています。
order_by()で並び替える
# 公開日が新しい順
Article.objects.order_by("-published_at")
# タイトルの昇順
Article.objects.order_by("title")
フィールド名の先頭に-(マイナス)を付けると降順になります。
つまずきやすいポイント:get()で例外が起きる
DoesNotExist例外
筆者は最初、get()を使って画面が真っ白になるエラーを何度も経験しました。
❌ Before:存在しないpkをget()で取得しようとする
def article_detail(request, pk):
article = Article.objects.get(pk=pk)
return render(request, "article_detail.html", {"article": article})
# 存在しないpkでアクセスすると
# django.core.exceptions.ObjectDoesNotExist: Article matching query does not exist.
存在しない主キーでアクセスされると、このDoesNotExist例外がそのまま画面に表示されてしまいます。
✅ After:get_object_or_404()を使って404ページを返す
from django.shortcuts import get_object_or_404, render
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(request, "article_detail.html", {"article": article})
get_object_or_404()を使えば、該当データがない場合に自動で404エラーページを返してくれます。
ビューでget()を直接使う場面では、基本的にget_object_or_404()に置き換えるくせをつけておくと安全です。
応用:Q objectsで複雑な条件を組む
「AでもBでもよい」というOR条件を書きたい場合は、Qオブジェクトを使います。
from django.db.models import Q
# タイトルに「Django」を含む、または「Python」を含む記事
Article.objects.filter(Q(title__icontains="Django") | Q(title__icontains="Python"))
filter()を単純に並べるとAND条件になってしまうため、OR条件を書きたいときはQオブジェクトが必須です。
まとめ
この記事のポイント
- QuerySetは遅延評価され、実際に使われるまでSQLは実行されない
filter()はAND条件、OR条件を組みたいときはQオブジェクトを使う__gte__icontainsなどのフィールドルックアップで細かい条件を指定できるget()は例外に注意し、ビューではget_object_or_404()を使うのが安全
次に読むべき記事
QuerySetの基本を理解したら、次はフォーム(ModelForm)でユーザー入力を受け取りバリデーションする方法を解説します。
→ 次の記事:フォーム(forms.Form/ModelForm)でバリデーションを実装する