Django中級 #1 Class-Based Views の深堀り
Django 基礎 7 編を終えたら、ここから一段階上に入ります。中級シリーズは 基礎で軽く触れたツールを本格的に扱う 7 編構成です。
- #1 Class-Based Views の深堀り ← 今回
- #2 ORM 中級 — annotate、aggregate、F/Q、prefetch_related
- #3 Signals と Middleware
- #4 ユーザー / 権限 — カスタム user model
- #5 メッセージ / セッション / クッキー
- #6 Static/Media の運用と storage backends
- #7 テスト — TestCase、fixtures、pytest-django
最初のテーマは Class-Based Views (CBV) です。基礎 #4 で関数ベースビュー (FBV) で書いたコードを、再利用可能なクラス で書き直す回です。
FBV vs CBV — なぜクラスなのか #
基礎 #4 で見た関数ベースビューは直感的です。リクエストを受けてレスポンスを返す関数 1 つ。ところが CRUD のようなありふれたパターン を複数のモデルに繰り返し使っていると、似たコードが増え続けます。
| FBV | CBV | |
|---|---|---|
| 定義 | 関数 1 つ | クラス + メソッド |
| 学習曲線 | 非常に低い | 少し高い |
| HTTP メソッドの分岐 | if request.method == 'POST' | def get、def post |
| 再利用 | デコレータ / 関数の分解 | 継承 / Mixin |
| Generic CRUD | 自前で実装 | ビルトイン (ListView など) |
| コードフローの追跡 | 上から下へ明確 | 親クラスを辿る必要あり |
CBV の強みは ありふれたパターンを親クラスに置き、差分だけをオーバーライド することです。短所はフローが親メソッドを経由するので、最初はどこがどう動いているのか追跡が難しい こと。
ルール 1 つ: 単純なルート (ヘルスチェック、単純なリダイレクト、小さなフォーム) は FBV、CRUD / リスト / 詳細のような定型パターンは CBV が普通の答えです。どちらか一方に統一する必要はありません。
CBV の出発点 — View
#
最も基本のクラスは django.views.View です。HTTP メソッドごとに get、post、put、delete メソッドを定義します。
from django.http import HttpResponse
from django.views import View
class HelloView(View):
def get(self, request):
return HttpResponse("Hello, GET")
def post(self, request):
return HttpResponse("Hello, POST")from django.urls import path
from .views import HelloView
urlpatterns = [
path('hello/', HelloView.as_view(), name='hello'),
]HelloView.as_view() が クラス → 呼び出し可能なビュー関数 に変換します。URL パターンは常に呼び出し可能なオブジェクトを受け取りますから。
dispatch — すべてのリクエストの入口
#
CBV はメソッド分岐の前に dispatch が一度呼び出されます。共通処理 (認証、ロギングなど) をここに入れられます。
class HelloView(View):
def dispatch(self, request, *args, **kwargs):
print(f"メソッド: {request.method}")
return super().dispatch(request, *args, **kwargs)
def get(self, request):
return HttpResponse("GET")TemplateView と RedirectView
#
最も短い CBV 2 つを先に押さえるとパターンが掴めます。
from django.views.generic import TemplateView
class AboutView(TemplateView):
template_name = "blog/about.html"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["page_title"] = "会社紹介"
return ctxfrom django.views.generic import RedirectView
class GoToBlogView(RedirectView):
pattern_name = "post_list" # url name 指定
permanent = False
query_string = True # クエリ文字列を引き継ぐget_context_data が テンプレートコンテキストを作るところ です。ほとんどすべての CBV がこのメソッドを持っています。
Generic CBV — CRUD のビルトイン #
ここからが CBV の本領です。Django がありふれた CRUD パターンを事前に用意したクラス群です。
ListView — 一覧ページ
#
基礎 #4 で関数型で書いた記事一覧を、CBV で書き直してみます。
from django.shortcuts import render
from .models import Post
def post_list(request):
posts = Post.objects.filter(published=True).order_by('-created_at')
return render(request, 'blog/post_list.html', {'posts': posts})from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
ordering = ["-created_at"]
def get_queryset(self):
return super().get_queryset().filter(published=True)主要なクラス属性:
model— どのモデルの一覧かtemplate_name— 書かなければ<app>/<model>_list.htmlを自動推定context_object_name— テンプレートで受け取る変数名 (デフォルトobject_list)paginate_by— 自動ページネーション (Django がpage_obj、paginatorをコンテキストに渡す)ordering— デフォルトの並び順
get_queryset をオーバーライドして 追加のフィルタ をかけます。URL クエリ文字列で検索条件を受け取るときも、ここで処理するときれいに収まります。
class PostListView(ListView):
model = Post
paginate_by = 10
def get_queryset(self):
qs = super().get_queryset().filter(published=True)
q = self.request.GET.get("q")
if q:
qs = qs.filter(title__icontains=q)
return qsDetailView — 単件取得
#
from django.views.generic import DetailView
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
context_object_name = "post"
slug_field = "slug"
slug_url_kwarg = "slug"
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["related"] = Post.objects.filter(
category=self.object.category
).exclude(pk=self.object.pk)[:5]
return ctxurlpatterns = [
path('posts/<int:pk>/', PostDetailView.as_view(), name='post_detail'),
# または slug ベース
path('posts/<slug:slug>/', PostDetailView.as_view(), name='post_detail'),
]DetailView は URL の pk または slug でオブジェクトを自動取得します。見つからなければ自動で 404。
CreateView、UpdateView、DeleteView
#
基礎 #6 の ModelForm パターンがそのまま自動化されます。
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
from .forms import PostForm
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
success_url = reverse_lazy("post_list")
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
def get_success_url(self):
return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})class PostDeleteView(DeleteView):
model = Post
template_name = "blog/post_confirm_delete.html"
success_url = reverse_lazy("post_list")主要なオーバーライドポイント:
form_valid(form)— フォームが通った直後。著者の自動指定、スラッグの自動生成などform_invalid(form)— 検証失敗時get_success_url()— 成功後のリダイレクト URL (動的ならメソッド、静的ならsuccess_url属性)get_form_kwargs()— フォームのコンストラクタに追加引数を渡す
reverse_lazy を使う理由は、クラス定義時点ではまだ URL conf がロードされていない可能性があるからです。呼び出し時点まで reverse を遅延させるのが lazy。
FormView — モデルなしのフォーム
#
from django.views.generic.edit import FormView
from .forms import ContactForm
class ContactView(FormView):
template_name = "blog/contact.html"
form_class = ContactForm
success_url = reverse_lazy("contact_done")
def form_valid(self, form):
form.send_email()
return super().form_valid(form)DB 保存なしで フォーム検証 + 後処理 だけが必要なケースに向きます。
Mixin — 小さな振る舞いの組み立て #
CBV の本当の力は Mixin にあります。小さな振る舞い (認証チェック、権限チェック、コンテキスト追加など) を別クラスに置いて 多重継承 で組み立てます。
LoginRequiredMixin — ログイン必須
#
from django.contrib.auth.mixins import LoginRequiredMixin
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
login_url = "/accounts/login/" # 未ログイン時に送る先 (デフォルト settings.LOGIN_URL)
redirect_field_name = "next"LoginRequiredMixin が最初 に来なければなりません。多重継承の MRO により検査ロジックが先に実行されるようにするためです。
PermissionRequiredMixin — 権限検査
#
from django.contrib.auth.mixins import PermissionRequiredMixin
class PostUpdateView(PermissionRequiredMixin, UpdateView):
model = Post
form_class = PostForm
permission_required = "blog.change_post"
raise_exception = True # なければ 403、True なら PermissionDeniedpermission_required は文字列またはリスト。権限システム自体は #4 ユーザー / 権限 で詳しく。
UserPassesTestMixin — 任意の条件
#
from django.contrib.auth.mixins import UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
def test_func(self):
post = self.get_object()
return post.author == self.request.usertest_func が True を返せば通過。オブジェクト単位の権限 (この記事の作成者のみ編集) のようなケースにきれいに収まります。
自前の Mixin #
class SidebarContextMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["recent_posts"] = Post.objects.order_by("-created_at")[:5]
ctx["popular_tags"] = Tag.objects.popular()[:10]
return ctx
class PostListView(SidebarContextMixin, ListView):
model = Post
class PostDetailView(SidebarContextMixin, DetailView):
model = Post複数のビューで共通して使うコンテキストを 1 か所に集めます。クラス継承の本来の効用が活きる場面です。
フローの追跡 — どこがどう動いているか #
CBV が難しく感じる最大の理由は 親クラスのメソッドフロー を知らないからです。ListView のフローを 1 度辿ってみます。
URL → as_view() → dispatch(request)
↓
get(request)
↓
self.object_list = self.get_queryset()
↓
context = self.get_context_data()
↓
self.render_to_response(context)オーバーライドするメソッド:
| メソッド | 役割 |
|---|---|
dispatch | すべてのメソッド共通の前処理 |
get_queryset | 一覧のクエリセット加工 (フィルタ、並び順) |
get_context_data | テンプレートに渡す追加データ |
get_template_names | テンプレート名を動的に決定 |
form_valid/form_invalid | フォーム処理フック (Create/Update) |
get_success_url | 成功後の遷移先 |
フローが混乱したら、公式ドキュメントの Class-based generic views ページや Classy CBV (ccbv.co.uk) サイトが大いに役立ちます。各クラスのすべての属性・メソッドが一目で整理されています。
URL 登録パターン #
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path('', views.PostListView.as_view(), name='post_list'),
path('posts/<slug:slug>/', views.PostDetailView.as_view(), name='post_detail'),
path('posts/new/', views.PostCreateView.as_view(), name='post_create'),
path('posts/<slug:slug>/edit/', views.PostUpdateView.as_view(), name='post_update'),
path('posts/<slug:slug>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
]as_view() はクラスメソッドなのでインスタンスを作らずに呼び出します。URL マッチング時点ごとに新しいインスタンスが作られます (リクエスト間でインスタンスの状態は共有されません)。
いつ FBV / いつ CBV #
| 状況 | 推奨 |
|---|---|
| ヘルスチェック、単純なリダイレクト、簡単な検索 | FBV |
| API 1〜2 つで終わる小さなエンドポイント | FBV |
| 標準 CRUD (一覧 / 詳細 / 作成 / 更新 / 削除) | CBV (Generic) |
| 権限・ログインのような横断的関心事の繰り返し | CBV (Mixin) |
| フローが複雑で分岐が多い | FBV (可読性) |
| 複数モデルに同じパターンが繰り返し | CBV (再利用) |
Django 陣営の普通の答え: 2 つを混ぜて使うのが自然。CBV が良いところは CBV で、FBV が良いところは FBV で。
まとめ #
今回押さえたもの:
- CBV の出発点 —
View+as_view()+ メソッドごとの分岐 dispatchがすべてのリクエストの入口TemplateView、RedirectView— 最も短い CBV- Generic CBV —
ListView、DetailView、CreateView、UpdateView、DeleteView、FormView - 主要なオーバーライド:
get_queryset、get_context_data、form_valid、get_success_url - Mixin —
LoginRequiredMixin、PermissionRequiredMixin、UserPassesTestMixin、自前のコンテキスト Mixin - Mixin は 継承順 が重要 (前に置いたものが先)
reverse_lazyはクラス定義時点で URL 解決を遅延させるツール- FBV/CBV は二者択一ではない — 状況に合わせて混ぜる
次回 (#2 ORM 中級) では、基礎 #3 のシンプルな QuerySet の上に annotate、aggregate、F/Q、select_related/prefetch_related のような本格的な ORM ツールを積み上げます。N+1 問題とその解決まで一カ所で。