基本

Django クラスベースビュー(CBV)完全ガイド

Django

Django クラスベースビュー
(CBV)完全ガイド

クラスベースビューを使えば、CRUDや一覧表示をわずか数行で実装できます。

こんな人向けの記事です

  • Django のビューをクラスで書きたい人
  • ListView、DetailView などの汎用ビューを使いたい人
  • 関数ビューとの違いを理解したい人

Step 1関数ビュー vs クラスベースビュー

views.py
# 関数ビュー(FBV)
from django.shortcuts import render, get_object_or_404
from .models import Article

def article_list(request):
    articles = Article.objects.filter(is_published=True)
    return render(request, 'blog/article_list.html', {'articles': articles})

def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    return render(request, 'blog/article_detail.html', {'article': article})


# クラスベースビュー(CBV) — 同じことをより簡潔に
from django.views.generic import ListView, DetailView

class ArticleListView(ListView):
    model = Article
    template_name = 'blog/article_list.html'
    context_object_name = 'articles'
    queryset = Article.objects.filter(is_published=True)

class ArticleDetailView(DetailView):
    model = Article
    template_name = 'blog/article_detail.html'
    context_object_name = 'article'
urls.py
from django.urls import path
from . import views

urlpatterns = [
    # 関数ビュー
    path('articles/', views.article_list),
    # クラスベースビュー(.as_view() が必要)
    path('articles/', views.ArticleListView.as_view()),
    path('articles/<int:pk>/', views.ArticleDetailView.as_view()),
]

Step 2TemplateViewとListView

views.py
from django.views.generic import TemplateView, ListView

# 静的ページ
class HomeView(TemplateView):
    template_name = 'home.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['title'] = 'ホームページ'
        return context

# 一覧表示(ページネーション付き)
class ArticleListView(ListView):
    model = Article
    template_name = 'blog/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10                  # 10件ずつ表示
    ordering = ['-published_at']      # 新しい順

    def get_queryset(self):
        qs = super().get_queryset().filter(is_published=True)
        # 検索キーワードがあればフィルタ
        keyword = self.request.GET.get('q')
        if keyword:
            qs = qs.filter(title__icontains=keyword)
        return qs
ページネーション
paginate_byを設定するだけで、テンプレートでpage_objを使ったページ送りが可能です。

Step 3DetailViewとCreateView

views.py
from django.views.generic import DetailView, CreateView
from django.urls import reverse_lazy
from .models import Article
from .forms import ArticleForm

# 詳細表示
class ArticleDetailView(DetailView):
    model = Article
    template_name = 'blog/article_detail.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['related'] = Article.objects.filter(
            classification=self.object.classification
        ).exclude(pk=self.object.pk)[:5]
        return context

# 新規作成
class ArticleCreateView(CreateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/article_form.html'
    success_url = reverse_lazy('article-list')

    def form_valid(self, form):
        form.instance.author = self.request.user  # ログインユーザーを設定
        return super().form_valid(form)

Step 4UpdateViewとDeleteView

views.py
from django.views.generic import UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin

# 更新
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
    model = Article
    form_class = ArticleForm
    template_name = 'blog/article_form.html'

    def get_success_url(self):
        return reverse_lazy('article-detail', kwargs={'pk': self.object.pk})

    def get_queryset(self):
        # 自分の記事のみ編集可能
        return super().get_queryset().filter(author=self.request.user)

# 削除
class ArticleDeleteView(LoginRequiredMixin, DeleteView):
    model = Article
    template_name = 'blog/article_confirm_delete.html'
    success_url = reverse_lazy('article-list')

    def get_queryset(self):
        return super().get_queryset().filter(author=self.request.user)
urls.py(CRUD全体)
urlpatterns = [
    path('', ArticleListView.as_view(), name='article-list'),
    path('<int:pk>/', ArticleDetailView.as_view(), name='article-detail'),
    path('create/', ArticleCreateView.as_view(), name='article-create'),
    path('<int:pk>/edit/', ArticleUpdateView.as_view(), name='article-update'),
    path('<int:pk>/delete/', ArticleDeleteView.as_view(), name='article-delete'),
]

Step 5Mixinでカスタマイズ

views.py
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

# ログイン必須
class MyView(LoginRequiredMixin, ListView):
    login_url = '/login/'
    model = Article

# 権限チェック
class AdminArticleView(PermissionRequiredMixin, UpdateView):
    permission_required = 'blog.change_article'
    model = Article

# カスタムMixin
class AuthorRequiredMixin:
    """投稿者のみアクセス可能にするMixin"""
    def get_queryset(self):
        return super().get_queryset().filter(author=self.request.user)

class MyArticleUpdateView(LoginRequiredMixin, AuthorRequiredMixin, UpdateView):
    model = Article
    form_class = ArticleForm

# JSON レスポンス用 Mixin
from django.http import JsonResponse

class JsonResponseMixin:
    def render_to_response(self, context, **kwargs):
        return JsonResponse(self.get_json_data(context))

    def get_json_data(self, context):
        return {'object': str(context['object'])}

class ArticleJsonView(JsonResponseMixin, DetailView):
    model = Article

Step 6実践パターン

views.py
from django.views.generic import FormView
from django.contrib import messages

# FormView: フォーム処理に特化
class ContactView(FormView):
    template_name = 'contact.html'
    form_class = ContactForm
    success_url = reverse_lazy('contact-done')

    def form_valid(self, form):
        form.send_email()
        messages.success(self.request, '送信しました')
        return super().form_valid(form)

# 複数のHTTPメソッドを処理
from django.views import View

class ArticleAPIView(View):
    def get(self, request, pk):
        article = get_object_or_404(Article, pk=pk)
        return JsonResponse({'title': article.title})

    def post(self, request):
        # 作成処理
        pass

    def delete(self, request, pk):
        article = get_object_or_404(Article, pk=pk)
        article.delete()
        return JsonResponse({'status': 'deleted'})
CBVの注意点
汎用ビューの内部動作(メソッド呼び出し順序)を理解していないと、カスタマイズ時に混乱します。ccbv.co.uk でソースコードを確認しましょう。

Step 7redirect() によるページ遷移

ビュー内でフォーム処理やデータ保存後に別ページへ遷移させたい場合は、redirect() を使います。

views.py
from django.shortcuts import redirect, render

class ArticleCreateView(View):
    def get(self, request):
        form = ArticleForm()
        return render(request, 'blog/article_form.html', {'form': form})

    def post(self, request):
        form = ArticleForm(request.POST)
        if form.is_valid():
            form.save()
            # urls.py の name で指定したページへリダイレクト
            return redirect('article-list')
        else:
            # バリデーション失敗時は元のページに戻す
            return render(request, 'blog/article_form.html', {'form': form})
redirect() のポイント
redirect() の引数には urls.py の name を渡します。URLを直接書くのではなく、name を使うことで URL 構成を変更しても影響を受けません。
なお、CBV の CreateViewUpdateView では success_url 属性で同様のリダイレクトを宣言的に設定できます(Step 3 参照)。

Step 8include() と namespace によるURL分割

アプリが増えると、すべての URL をプロジェクトの urls.py に書くのは管理しづらくなります。include() を使ってアプリごとに urls.py を分割し、namespace で名前空間を付けましょう。

プロジェクトの urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    # include() でアプリの urls.py を読み込み、namespace を設定
    path('blog/', include(('blog.urls', 'blog'), namespace='blog')),
    path('shop/', include(('shop.urls', 'shop'), namespace='shop')),
]
アプリの urls.py(blog/urls.py)
from django.urls import path
from . import views

urlpatterns = [
    path('', views.ArticleListView.as_view(), name='article-list'),
    path('<int:pk>/', views.ArticleDetailView.as_view(), name='article-detail'),
]
テンプレートやビューでの使い方
# ビューで namespace 付きの name を使う
return redirect('blog:article-list')

# テンプレートで namespace 付きの name を使う
# <a href="{% url 'blog:article-detail' article.pk %}">記事を見る</a>
namespace のメリット
複数アプリで同じ name(例: list)を使っても、blog:listshop:list のように区別できます。include() の第1引数はタプル ('アプリ.urls', 'app_name') の形式で指定します。

まとめ

  • ListView, DetailView でCRUD処理を簡潔に記述
  • paginate_by でページネーションを自動化
  • LoginRequiredMixin で認証チェックを追加
  • カスタムMixinで共通ロジックを再利用
  • URLパターンでは.as_view()を忘れずに
  • redirect()でname指定のページ遷移
  • include()とnamespaceでURL管理を整理