クラスビュー(CBV)リファクタリング

本章では、Djangoのクラスビュー(CBV)を学び、より簡潔なコードでホームページと詳細ページをリファクタリングします。


FBV vs CBV

これまでのビューはすべて関数で書いてきました(Function-Based View、FBV)。

Djangoにはさらにクラスビュー(Class-Based View、CBV)、クラスを用いてビューのロジックをカプセル化します。

特徴FBV(関数ビュー)CBV(クラスビュー)
コード量柔軟だが、よく使うロジックは繰り返し書く必要がある汎用ビューを継承すれば数行で済む
可読性直線的なフローで、ロジックが明確継承チェーンを追う必要があるが、慣れれば一目瞭然
再利用性関数でカプセル化Mixinの組み合わせ(Djangoの特徴)
適用シーンロジックが単純で、フローが明確なページCRUDのようなパターン化されたページ
HTTPメソッドの振り分け手動で if request.method == 'POST'自動的に get() / post() メソッドへ振り分け

両者に絶対的な優劣はなく、Djangoプロジェクトでは通常FBVとCBVを混在させる:複雑なロジックにはFBV、標準的なCRUDにはCBVを使用します。


ListView — リストページのリファクタリング

ホームページのindexビューは本質的に:記事リストの取得 → テンプレートの描画。

このようなパターン化されたロジックは、DjangoのListViewがすでに実装してくれています。

例

# ファイルパス:blog/views.py
from django.views.generic import ListView
from django.db.models import Q
from .models import Post, Category

class PostListView(ListView):
    """ブログのホームページ:ListViewを継承し、記事リストを表示"""
    model = Post                           # モデルを指定
    template_name = 'blog/index.html'       # テンプレートを指定(デフォルト:blog/post_list.html)
    context_object_name = 'posts'           # テンプレートで使用する変数名(デフォルト:object_list)
    paginate_by = 12                        # 1ページ12件(ページネーション)
    ordering = ['-created_at']             # 並び替え

    def get_queryset(self):
        """クエリメソッドをオーバーライド:カテゴリ絞り込み + キーワード検索に対応"""
        queryset = super().get_queryset()

        # カテゴリ絞り込み
        self.category_slug = self.request.GET.get('category', '')
        if self.category_slug:
            queryset = queryset.filter(category__slug=self.category_slug)

        # キーワード検索
        self.keyword = self.request.GET.get('q', '')
        if self.keyword:
            queryset = queryset.filter(
                Q(title__icontains=self.keyword) |
                Q(summary__icontains=self.keyword)
            )

        return queryset

    def get_context_data(self, **kwargs):
        """コンテキストメソッドをオーバーライド:テンプレートに追加データを渡す"""
        context = super().get_context_data(**kwargs)
        context['categories'] = Category.objects.all()
        context['category_slug'] = self.category_slug
        context['keyword'] = self.keyword
        context['title'] = 'サンプル ブログ - ホームページ'
        return context

リファクタリング前後のコード量を比較すると:FBV 約40行 → CBV 約30行になり、しかもロジックが異なるメソッドに明確に分割されています。


DetailView — 詳細ページのリファクタリング

例

# ファイルパス:blog/views.py に追加
from django.views.generic import DetailView

class PostDetailView(DetailView):
    """記事詳細ページ:DetailViewを継承"""
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

    # pk_url_kwarg:URL内で主キーを渡すパラメータ名(デフォルトは pk、ここでは明示的に宣言)
    pk_url_kwarg = 'pk'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = f'{self.object.title} - サンプル ブログ'
        return context

ルーティング設定の更新

CBVのルーティングの書き方はFBVとは少し異なります——呼び出す必要があります.as_view()。

例

# ファイルパス:blog/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    # CBV:クラスの後に .as_view() を付けてビュー関数に変換する
    path('', views.PostListView.as_view(), name='index'),
    path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail'),

    # 他のビューは変更なし
    path('register/', views.register, name='register'),
    path('login/', auth_views.LoginView.as_view(
        template_name='blog/login.html',
        redirect_authenticated_user=True
    ), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
]

なぜCBVでは.as_view()を使うのか?DjangoのURL設定は呼び出し可能オブジェクト(関数)を期待します。.as_view()クラスビューをDjangoのビュー関数シグネチャに適合する呼び出し可能オブジェクトに変換し、内部でHTTPメソッド(GET/POST)に応じてクラスの get() または post() メソッドに自動的に振り分けます。


よく使う汎用ビュー一覧

ビューテンプレートのデフォルト名用途
ListViewモデル名_list.htmlオブジェクトのリストを表示
DetailViewモデル名_detail.html単一オブジェクトの詳細を表示
CreateViewモデル名_form.html新しいオブジェクトを作成(フォームを含む)
UpdateViewモデル名_form.html既存のオブジェクトを更新
DeleteViewモデル名_confirm_delete.htmlオブジェクト削除の確認
TemplateView指定が必要静的テンプレートを描画(データ取得なし)

Djangoの汎用ビューはWeb開発の約80%のシーンをカバーしています。要件を満たさない場合はFBVに戻ります。


LoginRequiredMixin

CBVでは@login_requiredデコレータ(それは関数用のもの)は使えません。代わりにLoginRequiredMixin。

例

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView

class FavoriteListView(LoginRequiredMixin, ListView):
    """お気に入りリスト:ログインが必要"""
    model = Post
    template_name = 'blog/favorites.html'
    context_object_name = 'posts'
    ordering = ['-created_at']

    # LoginRequiredMixin 未ログインユーザーは自動的に settings.LOGIN_URL へリダイレクトされる

    def get_queryset(self):
        # 現在のユーザーがお気に入りした記事のみ表示
        return self.request.user.favorite_posts.all().order_by('-created_at')

Mixinの継承順序は重要です:LoginRequiredMixin必ずListView前に書く必要があります。Pythonの多重継承の解決順序は左から右で、LoginRequiredMixinが先にログイン状態をチェックし、通過した後にListViewのロジックを実行します。


CBVはいつ使い、FBVはいつ使うのか?

経験則:

  • FBV:ロジックが複雑で、条件分岐が多く、標準的なフローではないビュー(例:toggle_favorite)
  • CBV:標準的なCRUD操作、ロジックを get/post に明確に分割できるビュー(例:リストページ、詳細ページ)

toggle_favorite(お気に入り切り替え)のような操作は、ロジックは単純ですがフローが通常のCRUDとは異なるため、FBVの方が適しています。

index や post_detail のような標準的なデータ表示ページでは、CBVがボイラープレートコードを大幅に削減できます。


本章のまとめ

本章では、Djangoのクラスビューの核心的な使い方を学びました:ListViewでリストを表示、DetailViewで詳細を表示、get_queryset/get_context_dataでクエリとコンテキストをカスタマイズ、as_view() でルーティングに適用、LoginRequiredMixin で login_required を代替します。

リファクタリング後のホームページと詳細ページのコードはより短く、構造がより明確になりました。

その他の拡張