【Django】QuerySetの基本とフィルタの書き方

Django

こんにちは、かつコーチです。

前回は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リスト内のいずれかに一致
__isnullNULLかどうか

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)でバリデーションを実装する

タグ: #Django #初心者向け #データベース

タイトルとURLをコピーしました