Django超初心者シリーズ

8回 / 全10

【Django CBV入門】クラスベースビューで効率的な開発を実現する方法

2025/07/24に公開

はじめに:同じコードを何度も書いていませんか?

これまでの連載で、様々な機能を関数ビューで実装してきました。でも、気づいていましたか?僕たちは同じようなコードを何度も書いています。

例えば、第7回までに実装した各ビュー関数を見てみましょう:

# post_list関数の一部 def post_list(request): posts = Post.objects.filter(is_published=True).order_by("-created_at") categories = Category.objects.all().order_by("name") # ... # post_detail関数の一部 def post_detail(request, post_id): # ... categories = Category.objects.all().order_by("name") # また同じ! # ... # category_posts関数の一部 def category_posts(request, slug): # ... categories = Category.objects.all().order_by("name") # またまた同じ! # ...

「これ、なんとかならないの?」そう感じたあなたは正しいです!

今回は、Djangoのクラスベースビューを使って、この問題を解決する方法を学びます。

Tip

FBVとCBV

Djangoには2つのビューの書き方があります:

  • FBV(Function-Based View):関数ベースビュー(これまで使ってきた方法)
  • CBV(Class-Based View):クラスベースビュー(今回学ぶ方法)

どちらも同じことができますが、書き方と考え方が違います。

Note

初めてこの記事を読む方へ:

この記事は連載の第8回目です。フォームの基本がまだの方は、第7回の記事をご覧ください。もしすぐにCBVの実装を始めたい方は、前回GitHubコードを参考にしながら進めることもできます。

この記事で学べること

  • 関数ベースビュー(FBV)とクラスベースビュー(CBV)の違い
  • 基本的なCBVの使い方
    • TemplateView:静的なページ表示
    • ListView:一覧表示
    • DetailView:詳細表示
  • 実践的な機能の実装
    • Aboutページの新規作成
    • 月別アーカイブ機能の追加
    • 既存ビューのCBV化
  • Mixinを使った共通処理の再利用

それでは、より効率的なDjango開発の世界へ進んでみましょう!

CBVって何?まずは見比べてみよう

同じ機能を両方で実装してみる

まず、簡単な「About」ページを関数ビューとクラスビューの両方で実装して、違いを見てみましょう。

関数ベースビュー(FBV)の場合

def about(request): """Aboutページ(関数版)""" context = { "title": "このブログについて", "author": "Django学習者", "created_year": 2025, } return render(request, "blog/about.html", context)

クラスベースビュー(CBV)の場合

from django.views.generic import TemplateView class AboutView(TemplateView): """Aboutページ(クラス版)""" template_name = "blog/about.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = "このブログについて" context["author"] = "Django学習者" context["created_year"] = 2025 return context

一見すると、CBVの方が長く見えますね。でも、これには理由があるんです!

CBVのメリット

  1. 構造化されている

    • テンプレート名はtemplate_name
    • データ準備はget_context_data()
    • 各処理が決まった場所にある
  2. 継承で機能を再利用できる

    • 共通処理を親クラスに書ける
    • 必要な部分だけ上書き(オーバーライド)
  3. Djangoが用意した便利機能が使える

    • ページネーション
    • 404エラー処理
    • フォーム処理

基本的なCBVを使ってみよう

TemplateView:シンプルなページ表示

まずは、ブログに「About」ページを追加してみましょう。

blog/views.pyに追加:

blog/views.py
from django.views.generic import TemplateView # 追加 # 既存の関数ビューはそのまま残す class AboutView(TemplateView): """Aboutページ""" template_name = "blog/about.html" # 使うテンプレートを指定 def get_context_data(self, **kwargs): # 親クラスのメソッドを呼ぶ(おまじないのようなもの) context = super().get_context_data(**kwargs) # テンプレートに渡したいデータを追加 context["title"] = "このブログについて" context["author"] = "techarm" context["created_year"] = 2025 return context
Tip

super()って何?

super()は「親クラスの機能を使う」という意味です。

メソッドをオーバーライド(上書き)するときに使います:

def get_context_data(self, **kwargs): # メソッドを上書き context = super().get_context_data(**kwargs) # 親クラスの処理を呼ぶ # 自分の処理を追加 return context

クラス変数だけ設定する場合はsuper()は不要です:

class MyView(TemplateView): template_name = "my_template.html" # クラス変数を設定するだけ # super()は使わない

blog/templates/blog/about.htmlを作成:

blog/templates/blog/about.html
{% extends 'blog/base.html' %} {% block title %}About - My Blog{% endblock %} {% block content %} <div class="about-page"> <h2>{{ title }}</h2> <div class="about-content"> <p>こんにちは!{{ author }}です。</p> <p>このブログは、Djangoの学習過程を記録するために{{ created_year }}年に開設しました。</p> <h3>学習している内容</h3> <ul> <li>Djangoの基本的な使い方</li> <li>MVTパターンの理解</li> <li>データベース操作(ORM)</li> <li>テンプレートシステム</li> <li>フォーム処理</li> <li>そして今、クラスベースビュー!</li> </ul> <p>一緒にDjangoを学んでいきましょう!</p> </div> </div> {% endblock %}

blog/urls.pyに追加:

blog/urls.py
urlpatterns = [ # 既存のパターン path("", views.post_list, name="post_list"), path("post/<int:post_id>/", views.post_detail, name="post_detail"), path("category/<slug:slug>/", views.category_posts, name="category_posts"), path("search/", views.post_search, name="post_search"), # 新規追加(as_view()を忘れずに!) path("about/", views.AboutView.as_view(), name="about"), ]
Important

as_view()を忘れずに!

CBVをURLに登録するときは、必ず.as_view()を付けます:

path("about/", views.AboutView.as_view(), name="about"), # 正しい path("about/", views.AboutView, name="about"), # エラーになる!

これはDjangoのルールなので、覚えておいてください。

ナビゲーションにもリンクを追加してみましょう(blog/templates/blog/base.html):

<li><a href="{% url 'about' %}">About</a></li>

AboutページのスタイルをCSSに追加

blog/static/blog/css/style.cssに以下を追加:

blog/static/blog/css/style.css(追加分)
/* Aboutページ */ .about-page { max-width: 800px; margin: 0 auto; } .about-page h2 { color: #2c3e50; font-size: 2.5rem; margin-bottom: 2rem; text-align: center; } .about-content { background: white; padding: 3rem; border-radius: 8px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); line-height: 1.8; } .about-content h3 { color: #3498db; margin-top: 2rem; margin-bottom: 1rem; } .about-content ul { list-style-position: inside; margin-left: 1rem; } .about-content li { margin-bottom: 0.5rem; }

ブラウザで http://localhost:8000/about/ にアクセスして確認してみましょう!

Aboutページの表示

Note

「あれ?CBVってそんなにメリットある?」と思った方へ

Aboutページの例を見て、「関数ビューの方が短くて簡単じゃない?」と思ったかもしれません。

その通りです!TemplateViewのような単純なページ表示では、CBVのメリットはまだ感じにくいです。

でも、CBVにはたくさんの種類があり、TemplateViewは最もシンプルなものの一つです。 次に紹介するListViewDetailViewを使ってみると、CBVの本当の強さと便利さが実感できるはずです。

特に、ページネーションや404エラー処理など、関数ビューでは自分で実装する必要がある機能が、CBVでは自動的に提供されます!

ListView:一覧表示を簡単に

次に、月別アーカイブ機能を追加してみましょう。ListViewは「〇〇の一覧」を表示するのに特化したCBVです。

blog/views.pyAboutViewクラスの下にMonthArchiveViewクラスを追加してみましょう:

blog/views.py
from django.views.generic import TemplateView, ListView # ListViewを追加 class MonthArchiveView(ListView): """月別アーカイブ""" model = Post # どのモデルを表示するか template_name = "blog/archive_month.html" # 使うテンプレート context_object_name = "posts" # テンプレートで使う変数名 paginate_by = 10 # 1ページに表示する件数 def get_queryset(self): """表示するデータを取得""" # URLから年月を取得 year = self.kwargs.get("year") month = self.kwargs.get("month") # 該当月の公開記事を取得 return Post.objects.filter( is_published=True, created_at__year=year, created_at__month=month ).order_by("-created_at") def get_context_data(self, **kwargs): """テンプレートに渡すデータを追加""" context = super().get_context_data(**kwargs) # 年月の情報を追加 context["year"] = self.kwargs.get("year") context["month"] = self.kwargs.get("month") # サイドバー用のカテゴリー context["categories"] = Category.objects.all().order_by("name") return context
Note

ListViewの便利な機能

ListViewは自動的に以下の機能を提供してくれます:

  1. ページネーションpaginate_by = 10と書くだけ!
  2. 空の時の処理:データがない時も適切に処理
  3. テンプレート変数page_objis_paginatedなどを自動で用意

関数ビューだと自分で実装する必要がある機能が、最初から使えるんです!

blog/templates/blog/archive_month.htmlを作成:

blog/templates/blog/archive_month.html
{% extends 'blog/base.html' %} {% block title %}{{ year }}年{{ month }}月の記事 - My Blog{% endblock %} {% block content %} <div class="archive-page"> <h2>{{ year }}年{{ month }}月の記事</h2> <p class="post-count"> {% if posts %} {{ posts|length }}件の記事が見つかりました {% else %} この月の記事はありません {% endif %} </p> <div class="posts-grid"> {% for post in posts %} {% include 'blog/includes/post_card.html' %} {% empty %} <p>{{ year }}年{{ month }}月に投稿された記事はありません。</p> {% endfor %} </div> <!-- ページネーション(ListViewが自動的に用意してくれる!) --> {% if is_paginated %} <nav class="pagination-nav"> <ul class="pagination"> {% if page_obj.has_previous %} <li> <a href="?page={{ page_obj.previous_page_number }}">前へ</a> </li> {% endif %} <li class="current"> {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} </li> {% if page_obj.has_next %} <li> <a href="?page={{ page_obj.next_page_number }}">次へ</a> </li> {% endif %} </ul> </nav> {% endif %} </div> {% endblock %}

blog/urls.pyに追加:

blog/urls.py
urlpatterns = [ # 既存のパターン(省略)... # アーカイブ path( "archive/<int:year>/<int:month>/", views.MonthArchiveView.as_view(), name="archive_month", ), ]

アーカイブページのスタイルをCSSに追加

blog/static/blog/css/style.cssに以下を追加:

blog/static/blog/css/style.css(追加分)
/* アーカイブページ */ .archive-page { max-width: 1200px; margin: 0 auto; } .archive-page h2 { color: #2c3e50; margin-bottom: 1rem; } .archive-page .post-count { color: #7f8c8d; margin-bottom: 2rem; font-size: 1.1rem; }

サイドバーにアーカイブリストを追加

せっかくなので、サイドバーにアーカイブへのリンクも追加してみましょう。

まず、アーカイブデータを準備する処理をpost_list関数に追加します:

blog/views.py
from django.db.models import Q, Count # Countを追加 from django.db.models.functions import TruncMonth # 追加 def post_list(request): """記事一覧を表示""" # 既存のコード... # 月別アーカイブを取得(追加) archives = ( Post.objects.filter(is_published=True) .annotate(month=TruncMonth("created_at")) .values("month") .annotate(count=Count("id")) .order_by("-month")[:12] ) context = { "posts": posts, "featured_posts": featured_posts, "total_posts": posts.count(), "categories": categories, "archives": archives, # 追加 } return render(request, "blog/post_list.html", context)

月別アーカイブ取得処理の詳しい解説

このコードは少し複雑に見えるかもしれませんね。特にannotateという見慣れない関数が出てきます。

Tip

annotate()とは?

annotate()は「注釈を付ける」という意味で、QuerySetの各オブジェクトに追加の情報を付け加える関数です。

例えば:

# 各記事にコメント数を追加 posts = Post.objects.annotate(comment_count=Count('comments')) # これで post.comment_count でコメント数が取得できる

それでは、月別アーカイブ取得の処理を順番に見ていきましょう:

archives = ( Post.objects.filter(is_published=True) # ① 公開済みの記事のみを取得 .annotate(month=TruncMonth("created_at")) # ② 各記事に「月」情報を追加 .values("month") # ③ 月の値だけを取り出してグループ化 .annotate(count=Count("id")) # ④ 各月の記事数をカウント .order_by("-month") # ⑤ 新しい月から順に並べる [:12] # ⑥ 最新12ヶ月分のみ取得 )

具体的なデータの変化を追ってみましょう:

最初のデータ(4つの記事があるとします):

[ Post(id=1, title="記事A", created_at="2025-07-15"), Post(id=2, title="記事B", created_at="2025-07-20"), Post(id=3, title="記事C", created_at="2025-06-10"), Post(id=4, title="記事D", created_at="2025-06-25"), ]

filter(is_published=True)

  • 公開済みの記事だけに絞り込まれる(今回は全て公開済みと仮定)

.annotate(month=TruncMonth("created_at"))

  • 各記事にmonthという新しい属性が追加される
  • TruncMonthは日付を月初めに変換(15日→1日)
[ Post(..., created_at="2025-07-15", month="2025-07-01"), Post(..., created_at="2025-07-20", month="2025-07-01"), Post(..., created_at="2025-06-10", month="2025-06-01"), Post(..., created_at="2025-06-25", month="2025-06-01"), ]

.values("month") ← 【ここが重要!】

  • Postオブジェクトから月の情報だけを抽出
  • オブジェクトではなく辞書のリストに変わる
[ {"month": datetime(2025, 7, 1)}, {"month": datetime(2025, 7, 1)}, # 同じ月が重複 {"month": datetime(2025, 6, 1)}, {"month": datetime(2025, 6, 1)}, # 同じ月が重複 ]

.annotate(count=Count("id"))

  • 同じ月でグループ化され、記事数がカウントされる
[ {"month": datetime(2025, 7, 1), "count": 2}, # 7月は2記事 {"month": datetime(2025, 6, 1), "count": 2}, # 6月は2記事 ]

⑤⑥ .order_by("-month")[:12]の後:

  • 新しい月順に並べ替えて、最新12ヶ月分だけ取得

最終的にarchivesには、月ごとの記事数をまとめたデータが入ります:

[ {"month": datetime(2025, 7, 1), "count": 2}, {"month": datetime(2025, 6, 1), "count": 2}, # ... 最大12ヶ月分 ]

このデータを使って、サイドバーに「2025年7月 (2)」のようなリンクを表示できるんです!

Note

他のビューにも追加が必要です

実は、post_detailcategory_postsMonthArchiveViewにも同じようにarchivesを追加する必要があります。

「えっ、全部に?面倒...」と思いましたか?

その通りです!これが関数ビューの限界なんです。でも安心してください、後でもっと良い方法を紹介していきます!

blog/templates/blog/includes/sidebar.htmlに追加:

blog/templates/blog/includes/sidebar.html
<!-- 既存のカテゴリーセクションの後に追加 --> <div class="sidebar-section"> <h3>アーカイブ</h3> <ul class="archive-list"> {% for archive in archives %} <li> <a href="{% url 'archive_month' year=archive.month.year month=archive.month.month %}"> {{ archive.month|date:"Y年n月" }} </a> <span class="post-count">({{ archive.count }})</span> </li> {% empty %} <li>アーカイブがありません</li> {% endfor %} </ul> </div>

ブラウザで http://localhost:8000/archive/2025/7/ にアクセスして確認してみましょう:

Tip

アーカイブ機能を効果的に確認する方法

アーカイブ機能の動作を確認するには、Admin画面で記事の作成日を調整すると良いでしょう:

  1. Admin画面(http://localhost:8000/admin/)にログイン
  2. 「記事」をクリックして記事一覧を表示
  3. 編集したい記事をクリック
  4. 「作成日」フィールドを変更(例:2025年6月、7月、8月など異なる月に設定)
  5. 保存後、各月のアーカイブページ(例:/archive/2025/7/)にアクセス

これにより、月別アーカイブが正しく機能していることを確認できます。

月別アーカイブページの表示

ListViewの使い方まとめ

ここまでで、ListViewの便利さを実感していただけたでしょうか?第7回の検索画面では、ページネーションを実装するために以下のようなコードを書きました:

# 第7回の検索画面(関数ビュー)でのページネーション実装 def post_search(request): # ... # ページネーション paginator = Paginator(posts, 10) # ① Paginatorオブジェクトを作成 page_number = request.GET.get("page") # ② URLからページ番号を取得 page_obj = paginator.get_page(page_number) # ③ ページオブジェクトを取得 context = { "page_obj": page_obj, # ... }

ところが、ListViewを使うと:

class MonthArchiveView(ListView): paginate_by = 10 # これだけ!

たった1行でページネーションが実装できてしまいます!ListViewが自動的に:

  • Paginatorオブジェクトの作成
  • URLからページ番号の取得
  • ページオブジェクトの生成
  • テンプレートへの変数の受け渡し(page_objis_paginatedなど)

すべてを処理してくれるんです。

get_querysetとget_context_dataの役割

ListViewを使いこなすには、2つの重要なメソッドの役割を理解することが大切です:

1. get_queryset():表示するデータを決める

実は、シンプルなケースではget_queryset()を書く必要はありません:

# シンプルな例:全ての記事を表示 class SimplePostListView(ListView): model = Post # これだけでOK! # get_queryset()は不要(Post.objects.all()が自動的に使われる)

しかし、以下のような場合はget_queryset()を使います:

# フィルタリングが必要な例 class PublishedPostListView(ListView): model = Post def get_queryset(self): # 公開済みの記事のみ、新しい順で表示 return Post.objects.filter(is_published=True).order_by("-created_at")

get_queryset()が必要なケース:

  • フィルタリング(公開/非公開、カテゴリ別など)
  • 並び順の指定
  • URLパラメータに基づく動的な絞り込み
  • 複雑なクエリ(JOIN、集計など)

2. get_context_data():追加のデータを渡す

def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # メインのデータ以外に必要な情報を追加 context["extra_info"] = "追加情報" return context

役割分担のイメージ:

  • model = Post:「基本的な材料」を指定
  • get_queryset():「メインディッシュの調理方法」を決める(必要な場合のみ)
  • get_context_data():「サイドメニューや調味料」を追加する

この2つのメソッドで必要な処理を書いたら、あとはListViewが:

  • ページネーション処理
  • 空のリストの処理
  • テンプレートへのデータ受け渡し
  • エラーハンドリング

などを自動的に処理してくれます。

Note

modelとget_querysetの関係

Q: get_queryset()を書く場合でもmodelを指定する必要がありますか?

A: いいえ、必須ではありません。以下のパターンがあります:

# パターン1:modelのみ指定(シンプル) class PostListView(ListView): model = Post # Post.objects.all()が使われる # パターン2:get_querysetのみ指定 class PostListView(ListView): def get_queryset(self): return Post.objects.filter(is_published=True) # modelは指定しなくてもOK # パターン3:両方指定(推奨) class PostListView(ListView): model = Post # 明示的で分かりやすい def get_queryset(self): return Post.objects.filter(is_published=True)

両方指定する(パターン3)が推奨される理由:

  • コードの可読性が高い(どのモデルを扱っているか一目瞭然)
  • Django管理コマンドなど、一部の機能がmodel属性を参照することがある
  • 将来の拡張性(他の開発者が理解しやすい)

Q: テンプレートではどんな変数名でアクセスしますか?

A: デフォルトではobject_listですが、より分かりやすい名前に変更できます:

class PostListView(ListView): model = Post # context_object_nameを指定しない場合 # テンプレートでは {{ object_list }} でアクセス class PostListView(ListView): model = Post context_object_name = "posts" # 変数名を指定 # テンプレートでは {{ posts }} でアクセス

context_object_nameを指定する方が、テンプレートが読みやすくなるのでおすすめです!

ListViewが提供する便利な変数まとめ

ListViewを使うと、テンプレートで以下の変数が自動的に使えるようになります:

変数名説明
object_list または context_object_nameデータのリスト{{ posts }}
page_obj現在のページ情報{{ page_obj.number }}
paginatorページネーター{{ paginator.num_pages }}
is_paginatedページ分割されているか{% if is_paginated %}

これらを自分で設定する必要がないのも、CBVの大きなメリットです。

つまり、ListViewは「一覧表示でよくやる処理」を全部引き受けてくれる、とても頼もしい存在なんです。自分で書くコードは最小限で、Djangoのベストプラクティスに従った実装が自動的に行われます。

既存の機能をCBVに書き換えてみよう

DetailView:記事詳細ページ

ここで、既存のpost_detail関数をDetailViewに書き換えて、違いを実感してみましょう。

現在の関数ベースビュー

blog/views.py(現在のpost_detail)
def post_detail(request, post_id): """記事詳細とコメント投稿""" post = get_object_or_404(Post, pk=post_id, is_published=True) comments = post.comments.filter(is_approved=True) # コメントフォームの処理 if request.method == "POST": form = CommentForm(request.POST) if form.is_valid(): comment = Comment( post=post, name=form.cleaned_data["name"], email=form.cleaned_data["email"], content=form.cleaned_data["content"], ) comment.save() messages.success(request, "コメントを投稿しました!") return redirect("post_detail", post_id=post.id) else: form = CommentForm() # サイドバー用データ recent_posts = Post.objects.filter(is_published=True).exclude(id=post_id).order_by("-created_at")[:5] categories = Category.objects.all().order_by("name") archives = Post.objects.filter(is_published=True).annotate(month=TruncMonth("created_at")).values("month").annotate(count=Count("id")).order_by("-month")[:12] context = { "post": post, "posts": recent_posts, "categories": categories, "archives": archives, "comments": comments, "comment_form": form, "comment_count": comments.count(), } return render(request, "blog/post_detail.html", context)

CBV版の実装

blog/views.py(新規追加)
from django.views.generic import TemplateView, ListView, DetailView # DetailViewを追加 class PostDetailView(DetailView): """記事詳細とコメント(CBV版)""" model = Post template_name = "blog/post_detail.html" context_object_name = "post" pk_url_kwarg = "post_id" # URLのパラメータ名を指定 def get_queryset(self): """公開記事のみ表示""" return super().get_queryset().filter(is_published=True) def get_context_data(self, **kwargs): """テンプレートに渡すデータを準備""" context = super().get_context_data(**kwargs) # コメント関連 comments = self.object.comments.filter(is_approved=True) context["comments"] = comments context["comment_count"] = comments.count() context["comment_form"] = CommentForm() # サイドバー用 context["posts"] = ( Post.objects.filter(is_published=True) .exclude(id=self.object.id) .order_by("-created_at")[:5] ) context["categories"] = Category.objects.all().order_by("name") context["archives"] = ( Post.objects.filter(is_published=True) .annotate(month=TruncMonth("created_at")) .values("month") .annotate(count=Count("id")) .order_by("-month")[:12] ) return context def post(self, request, *args, **kwargs): """コメント投稿の処理""" # 現在表示している記事オブジェクトを取得 # self.objectに保存しておくことで、このメソッド内で使えるようにする self.object = self.get_object() form = CommentForm(request.POST) if form.is_valid(): # フォームが正しい場合、新しいコメントを作成 comment = Comment( post=self.object, # self.objectには現在の記事が入っている name=form.cleaned_data["name"], email=form.cleaned_data["email"], content=form.cleaned_data["content"], ) comment.save() messages.success(request, "コメントを投稿しました!") return redirect("post_detail", post_id=self.object.id) # エラーがある場合、エラー情報を含むフォームを再表示 context = self.get_context_data() context["comment_form"] = form # エラーが含まれたフォームをセット return self.render_to_response(context)
Note

self.objectとself.get_object()について

DetailViewでは、表示対象のオブジェクト(この場合はPostオブジェクト)をself.objectに保存する仕組みになっています。

  • self.get_object():URLのパラメータ(post_id)から該当するPostオブジェクトを取得するメソッド
  • self.object:取得したオブジェクトを保存するインスタンス変数

GETリクエスト(通常のページ表示)では、DetailViewが自動的にself.objectを設定してくれます。 しかし、POSTリクエスト(フォーム送信)では自分で設定する必要があるので、 self.object = self.get_object()というコードを書いています。

FBVとCBVの比較

両者を比較してみましょう:

観点FBV(関数ビュー)CBV(クラスビュー)
構造1つの関数にすべて処理ごとにメソッドを分割
読みやすさ上から下に読める各メソッドの役割を理解する必要
拡張性関数全体をコピーして修正必要なメソッドだけ上書き
共通処理各関数で重複継承やMixinで共有可能
学習コスト低い(関数だけ)高い(クラスの概念が必要)
Tip

どっちを使うべき?

FBVがおすすめな場合:

  • シンプルな処理
  • 特殊なロジックが多い
  • チームがCBVに慣れていない

CBVがおすすめな場合:

  • 標準的なCRUD操作
  • 同じような処理が多い
  • 大規模なプロジェクト

僕は最初FBVばかり使っていましたが、CBVの便利さを知ってからは使い分けています!

CBV版を試してみる

URLパターンを一時的に変更して試してみましょう:

blog/urls.py
urlpatterns = [ # FBV版をコメントアウトして、CBV版を有効化 # path("post/<int:post_id>/", views.post_detail, name="post_detail"), path("post/<int:post_id>/", views.PostDetailView.as_view(), name="post_detail"), # 他のパターン... ]

ブラウザで記事詳細ページにアクセスして、同じように動作することを確認してみましょう!

Mixinで共通処理をスマートに

記事の冒頭で提示した問題を覚えていますか?

# 全てのビューで同じコードを書いている問題 categories = Category.objects.all().order_by("name") # また同じ!

実は、ここまで学んできたCBV(TemplateView、ListView、DetailView)だけでは、この「同じコードの重複」問題はまだ解決できていません。

確かにCBVは関数ビューより構造化されていて、ページネーションなどの便利な機能を提供してくれます。しかし、MonthArchiveViewでもPostDetailViewでも、相変わらず同じようにcategoriesを取得するコードを書いています。

この問題を解決するのがMixinです。

Mixinって何?

Mixinは「機能を提供する小さなクラス」です。他のクラスと組み合わせて使います。

例えるなら:

  • CBV = 基本の料理
  • Mixin = 調味料

調味料(Mixin)を加えることで、料理(CBV)に新しい味(機能)を追加できます!

CategoryListMixinを作る

blog/mixins.pyを作成:

blog/mixins.py
from .models import Category class CategoryListMixin: """カテゴリ一覧を自動的に追加するMixin""" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # すべてのビューにカテゴリ一覧を追加 context["categories"] = Category.objects.all().order_by("name") return context

Mixinを使ってビューを改善

blog/views.pyを修正:

blog/views.py
from .mixins import CategoryListMixin # 追加 # MonthArchiveViewを修正 class MonthArchiveView(CategoryListMixin, ListView): """月別アーカイブ(Mixin使用版)""" model = Post template_name = "blog/archive_month.html" context_object_name = "posts" paginate_by = 10 def get_queryset(self): year = self.kwargs.get('year') month = self.kwargs.get('month') return Post.objects.filter(is_published=True,created_at__year=year,created_at__month=month).order_by("-created_at") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # categoriesの取得は不要に! # context["categories"] = Category.objects.all().order_by("name") context["year"] = self.kwargs.get('year') context["month"] = self.kwargs.get('month') return context # PostDetailViewも修正(差分のみ記載) class PostDetailView(CategoryListMixin, DetailView): """記事詳細とコメント(CBV版)""" def get_context_data(self, **kwargs): """テンプレートに渡すデータを準備""" context = super().get_context_data(**kwargs) comments = self.object.comments.filter(is_approved=True) context["comments"] = comments context["comment_count"] = comments.count() context["comment_form"] = CommentForm() context["posts"] = ( Post.objects.filter(is_published=True) .exclude(id=self.object.id) .order_by("-created_at")[:5] ) # categoriesの取得は不要に! # context["categories"] = Category.objects.all().order_by("name") context["archives"] = ( Post.objects.filter(is_published=True) .annotate(month=TruncMonth("created_at")) .values("month") .annotate(count=Count("id")) .order_by("-month")[:12] ) return context
Important

Mixinを使う時の注意点

Mixinは必ず左側に書きます:

class AboutView(CategoryListMixin, TemplateView): # 正しい class AboutView(TemplateView, CategoryListMixin): # 動かない!

これはPythonの仕組みによるものです。左から順番に処理されると覚えておけばOK!

他の便利なMixinも作ってみる

blog/mixins.py(追加)
from django.db.models import Count from django.db.models.functions import TruncMonth from .models import Category, Post class ArchiveListMixin: """アーカイブ一覧を自動的に追加するMixin""" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 月別アーカイブを追加 context['archives'] = Post.objects.filter( is_published=True ).annotate( month=TruncMonth('created_at') ).values('month').annotate( count=Count('id') ).order_by('-month')[:12] return context class SidebarMixin(CategoryListMixin, ArchiveListMixin): """サイドバー用のデータをまとめたMixin""" pass # CategoryListMixinとArchiveListMixinの機能を合体!

これで、SidebarMixinを使うだけで、カテゴリーとアーカイブの両方が自動的に追加されます!

実践:記事一覧もCBVにしてみよう

最後に、記事一覧もCBVに書き換えてみましょう:

blog/views.py(追加)
class PostListView(SidebarMixin, ListView): """記事一覧(CBV版)""" model = Post template_name = "blog/post_list.html" context_object_name = "posts" paginate_by = 10 def get_queryset(self): # 公開記事のみ、新しい順 return Post.objects.filter(is_published=True).order_by("-created_at") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # 注目記事 context["featured_posts"] = Post.objects.filter( is_published=True, is_featured=True ).order_by("-created_at")[:2] # 総記事数 context["total_posts"] = self.get_queryset().count() return context

見てください!カテゴリーとアーカイブの取得コードが消えて、すっきりしました!

URLパターンの更新

blog/urls.pyを修正して、CBV版を使うようにします:

blog/urls.py
urlpatterns = [ # FBV版をコメントアウトして、CBV版を有効化 # path("", views.post_list, name="post_list"), path("", views.PostListView.as_view(), name="post_list"), # 他のパターンはそのまま... ]

テンプレートにページネーションを追加

blog/templates/blog/post_list.htmlの記事一覧の後に、ページネーション部分を追加します:

blog/templates/blog/post_list.html(一部)
<!-- 既存の記事一覧表示の後に追加 --> <!-- ページネーション --> {% if is_paginated %} <nav class="pagination-nav"> <ul class="pagination"> {% if page_obj.has_previous %} <li> <a href="?page={{ page_obj.previous_page_number }}" class="page-link"> <i class="fas fa-chevron-left"></i> 前へ </a> </li> {% endif %} <!-- ページ番号の表示 --> <li class="page-info"> ページ {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} </li> {% if page_obj.has_next %} <li> <a href="?page={{ page_obj.next_page_number }}" class="page-link"> 次へ <i class="fas fa-chevron-right"></i> </a> </li> {% endif %} </ul> </nav> {% endif %}
Tip

ListViewの自動ページネーション

paginate_by = 10を設定するだけで、ListViewは自動的に:

  • 10件ずつに分割
  • page_objis_paginatedをテンプレートに渡す
  • URLパラメータ?page=2などを処理

これらすべてを自動で行ってくれます!関数ビューでは自分で実装する必要があった処理が、たった1行で完了です。

これで、記事一覧ページもCBV化が完了しました。SidebarMixinのおかげで、カテゴリーとアーカイブのデータも自動的に追加されています。

まとめ

今回は、Djangoのクラスベースビュー(CBV)について学びました:

できるようになったこと

  1. TemplateViewでシンプルなページを作成
  2. ListViewで一覧表示(ページネーション付き)
  3. DetailViewで詳細表示
  4. Mixinで共通処理をまとめる

解決した問題

冒頭で提示した「同じコードを何度も書く」問題は、MixinとCBVで解決されました:

Before(すべてのビューで)

categories = Category.objects.all().order_by("name")

After(Mixinを使えば)

class MyView(CategoryListMixin, ListView): # categoriesは自動的に追加される!

実践的なアドバイス

  1. 無理にすべてをCBVにしない

    • 既存のFBVが動いているならそのままでもOK
    • 新機能から徐々にCBVを使ってみる
  2. 最初はシンプルに

    • まずは基本的な使い方を覚える
    • 慣れてきたらMixinを活用
  3. 適材適所で使い分ける

    • シンプルな処理 → FBV
    • 標準的な一覧・詳細表示 → CBV

CBVは最初は難しく感じるかもしれませんが、慣れると開発効率が大幅に向上します。特に、Mixinで共通処理をまとめられるようになると、コードの重複が劇的に減ります。

僕も最初は「関数の方が分かりやすい」と思っていましたが、今では用途に応じて使い分けています。皆さんも、少しずつCBVに慣れていってください!

完成したコード

もし実行でエラーが出たり、不明な点がある場合は、以下の完成後のコードと比較してみてください:

https://github.com/techarm/django-blog-tutorial/tree/08-class-based-views

特にviews.pyのCBVの実装、mixins.pyの構造、urls.pyでのas_view()の使い方を確認することで、問題を解決できるでしょう。

次回予告

次回は、「ModelFormとGeneric CBV」について学びます!

  • ModelFormで自動的にフォームを生成
  • CreateView、UpdateView、DeleteViewの活用
  • 記事の作成・編集・削除機能の実装
  • ユーザー認証と権限管理の基礎

いよいよ、管理画面を使わずに記事を投稿できるようになります。本格的なブログシステムの完成まであと少し!次回もお楽しみに!🚀

この記事はいかがでしたか?

もしこの記事が参考になりましたら、
高評価をいただけると大変嬉しいです!

--

皆様からの応援が励みになります。ありがとうございます! ✨

Django超初心者シリーズについて

このシリーズでは、プログラミング初心者の方でもDjangoを使ったWebアプリケーション開発ができるようになることを目標としています。わからないことがあれば、コメント欄でお気軽にご質問ください!