Django中級 #1 Class-Based Views の深堀り

読了 7分

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 のようなありふれたパターン を複数のモデルに繰り返し使っていると、似たコードが増え続けます。

FBVCBV
定義関数 1 つクラス + メソッド
学習曲線非常に低い少し高い
HTTP メソッドの分岐if request.method == 'POST'def getdef post
再利用デコレータ / 関数の分解継承 / Mixin
Generic CRUD自前で実装ビルトイン (ListView など)
コードフローの追跡上から下へ明確親クラスを辿る必要あり

CBV の強みは ありふれたパターンを親クラスに置き、差分だけをオーバーライド することです。短所はフローが親メソッドを経由するので、最初はどこがどう動いているのか追跡が難しい こと。

ルール 1 つ: 単純なルート (ヘルスチェック、単純なリダイレクト、小さなフォーム) は FBVCRUD / リスト / 詳細のような定型パターンは CBV が普通の答えです。どちらか一方に統一する必要はありません。

CBV の出発点 — View #

最も基本のクラスは django.views.View です。HTTP メソッドごとに getpostputdelete メソッドを定義します。

blog/views.py — 最も基本
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")
blog/urls.py — as_view()
from django.urls import path
from .views import HelloView

urlpatterns = [
    path('hello/', HelloView.as_view(), name='hello'),
]

HelloView.as_view()クラス → 呼び出し可能なビュー関数 に変換します。URL パターンは常に呼び出し可能なオブジェクトを受け取りますから。

dispatch — すべてのリクエストの入口 #

CBV はメソッド分岐の前に dispatch が一度呼び出されます。共通処理 (認証、ロギングなど) をここに入れられます。

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")

TemplateViewRedirectView #

最も短い CBV 2 つを先に押さえるとパターンが掴めます。

TemplateView — 静的ページ
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 ctx
RedirectView — リダイレクトのみ
from 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 で書き直してみます。

🚫 FBV — 自前で書く
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})
✅ CBV — ListView
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_objpaginator をコンテキストに渡す)
  • 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 qs

DetailView — 単件取得 #

DetailView
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 ctx
urls.py — pk または slug
urlpatterns = [
    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

CreateViewUpdateViewDeleteView #

基礎 #6 の ModelForm パターンがそのまま自動化されます。

CreateView
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)
UpdateView
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})
DeleteView
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 — モデルなしのフォーム #

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 なら PermissionDenied

permission_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.user

test_funcTrue を返せば通過。オブジェクト単位の権限 (この記事の作成者のみ編集) のようなケースにきれいに収まります。

自前の Mixin #

共通コンテキスト 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 度辿ってみます。

ListView のフロー (簡略化)
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 登録パターン #

blog/urls.py — CBV 登録
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 がすべてのリクエストの入口
  • TemplateViewRedirectView — 最も短い CBV
  • Generic CBVListViewDetailViewCreateViewUpdateViewDeleteViewFormView
  • 主要なオーバーライド: get_querysetget_context_dataform_validget_success_url
  • MixinLoginRequiredMixinPermissionRequiredMixinUserPassesTestMixin、自前のコンテキスト Mixin
  • Mixin は 継承順 が重要 (前に置いたものが先)
  • reverse_lazy はクラス定義時点で URL 解決を遅延させるツール
  • FBV/CBV は二者択一ではない — 状況に合わせて混ぜる

次回 (#2 ORM 中級) では、基礎 #3 のシンプルな QuerySet の上に annotate、aggregate、F/Q、select_related/prefetch_related のような本格的な ORM ツールを積み上げます。N+1 問題とその解決まで一カ所で。

X