こんにちは、かつコーチです。
「開発中は普通に動いていたのに、本番で存在しない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, 中級者向け, エラー解決