【Django】DoesNotExist例外で落ちる画面を防ぐ方法

Django

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

「開発中は普通に動いていたのに、本番で存在しないIDにアクセスされたら500エラー画面になった」という経験はありませんか。
筆者も実際に、URLの数字を適当に書き換えてアクセスされただけでサーバーエラーになるサイトを作ってしまい、公開後に慌てて直したことがあります。

この記事では、Djangoで頻出するDoesNotExist例外の仕組みと、正しくエラーページ(404)に落とし込む方法を解説します。

DoesNotExist例外とは何か

モデルが見つからないときに発生する例外

DoesNotExistとは、Model.objects.get()で指定した条件に一致するレコードがDBに存在しないときに発生する例外です。
Djangoのモデルはそれぞれ、自分専用のDoesNotExistという例外クラスを自動的に持っています。

from blog.models import Post

try:
    post = Post.objects.get(pk=999)
except Post.DoesNotExist:
    print("該当する投稿がありません")

なぜ「落ちる」のか

問題は、この例外をキャッチせずに放置したままget()を呼んでいるビューです。
存在しないIDでアクセスされた瞬間に例外がそのまま外へ飛び出し、Djangoが500エラー(サーバーエラー)として処理してしまいます。

本来「そのページは存在しません」という404エラーで案内すべき場面なのに、サーバー側の異常を示す500エラーになってしまうのは、ユーザー体験としてもSEO的にも望ましくありません。

よくあるつまずきポイント・エラー対処

get()を無防備に使ってしまう

❌Before

def post_detail(request, pk):
    post = Post.objects.get(pk=pk)  # 存在しないpkだと例外が発生する
    return render(request, "blog/detail.html", {"post": post})

存在しないpkでアクセスされると、次のようなエラーが本番でも表示されてしまいます。

DoesNotExist at /post/999/
Post matching query does not exist.

これはDEBUG=Trueのときに見える画面で、本番(DEBUG=False)では味気ない「Server Error (500)」という画面になり、ユーザーは何が起きたのか分かりません。

✅After(get_object_or_404を使う)

from django.shortcuts import render, get_object_or_404
from blog.models import Post

def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, "blog/detail.html", {"post": post})

get_object_or_404は、Djangoが用意しているショートカット関数です。
対象が見つからない場合、DoesNotExistを発生させる代わりに自動的に404エラーページを返してくれます。
1行書き換えるだけで済むため、get()を使う場所ではまず候補に入れましょう。

自力でtry/exceptを書く場合

get_object_or_404が使えない複雑な条件のときは、自分でtry/exceptを書いてHttp404を送出することもできます。

from django.http import Http404
from django.shortcuts import render
from blog.models import Post

def post_detail(request, pk):
    try:
        post = Post.objects.get(pk=pk, is_published=True)
    except Post.DoesNotExist:
        raise Http404("公開されている投稿が見つかりません")
    return render(request, "blog/detail.html", {"post": post})

raise Http404(...)とすることで、DoesNotExistのような未処理例外ではなく、意図した404レスポンスに変換できます。

クラスベースビュー(DetailView)はデフォルトで対応済み

DetailViewなどの汎用ビューを使っている場合、実は内部でget_object_or_404相当の処理がすでに組み込まれています。

from django.views.generic import DetailView
from blog.models import Post

class PostDetailView(DetailView):
    model = Post
    template_name = "blog/detail.html"

このコードだけで、存在しないpkにアクセスされたときは自動的に404が返ります。
「関数ベースビューを書いていたときは気をつけていたのに、CBVに慣れてから逆に油断してget()を素で書いてしまう」というのもよくあるので、関数ベースに戻したときは注意が必要です。

応用・一歩先の使い方

複数条件の絞り込みでも404にできる

get_object_or_404は、filter()と同じように複数条件を渡せます。
「公開済みかつ自分が投稿したもの」のように条件を絞り込みたい場合にも便利です。

post = get_object_or_404(Post, pk=pk, is_published=True, author=request.user)

list系でも似たような考慮が必要

一覧取得でも、get_list_or_404という同様のショートカットが用意されています。
条件に一致するデータが1件もない場合に404を返したいときに活用できます。

from django.shortcuts import get_list_or_404

posts = get_list_or_404(Post, category=category_slug)

まとめ

この記事のポイント

  • Model.objects.get()は該当データがないとDoesNotExist例外を発生させる
  • 未処理のまま放置すると、本番で500エラー画面になりユーザー体験を損なう
  • get_object_or_404を使えば1行で適切な404レスポンスに変換できる
  • DetailViewなどの汎用ビューはデフォルトで404処理が組み込まれている

次に読むべき記事

エラーハンドリング全般をもう一度整理したい方は、「Djangoでよく出るエラーと解決法まとめ」もあわせてご覧ください。

タグ: Django, 中級者向け, エラー解決

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