【Django CBV入門】クラスベースビューで効率的な開発を実現する方法
はじめに:同じコードを何度も書いていませんか?
これまでの連載で、様々な機能を関数ビューで実装してきました。でも、気づいていましたか?僕たちは同じようなコードを何度も書いています。
例えば、第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のクラスベースビューを使って、この問題を解決する方法を学びます。
FBVとCBV
Djangoには2つのビューの書き方があります:
- FBV(Function-Based View):関数ベースビュー(これまで使ってきた方法)
- CBV(Class-Based View):クラスベースビュー(今回学ぶ方法)
どちらも同じことができますが、書き方と考え方が違います。
初めてこの記事を読む方へ:
この記事は連載の第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のメリット
-
構造化されている
- テンプレート名は
template_name
に - データ準備は
get_context_data()
に - 各処理が決まった場所にある
- テンプレート名は
-
継承で機能を再利用できる
- 共通処理を親クラスに書ける
- 必要な部分だけ上書き(オーバーライド)
-
Djangoが用意した便利機能が使える
- ページネーション
- 404エラー処理
- フォーム処理
基本的なCBVを使ってみよう
▶TemplateView:シンプルなページ表示
まずは、ブログに「About」ページを追加してみましょう。
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
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
を作成:
{% 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
に追加:
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"),
]
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
に以下を追加:
/* 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/ にアクセスして確認してみましょう!
「あれ?CBVってそんなにメリットある?」と思った方へ
Aboutページの例を見て、「関数ビューの方が短くて簡単じゃない?」と思ったかもしれません。
その通りです!TemplateViewのような単純なページ表示では、CBVのメリットはまだ感じにくいです。
でも、CBVにはたくさんの種類があり、TemplateViewは最もシンプルなものの一つです。 次に紹介するListViewやDetailViewを使ってみると、CBVの本当の強さと便利さが実感できるはずです。
特に、ページネーションや404エラー処理など、関数ビューでは自分で実装する必要がある機能が、CBVでは自動的に提供されます!
▶ListView:一覧表示を簡単に
次に、月別アーカイブ機能を追加してみましょう。ListViewは「〇〇の一覧」を表示するのに特化したCBVです。
blog/views.py
のAboutView
クラスの下にMonthArchiveView
クラスを追加してみましょう:
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
ListViewの便利な機能
ListViewは自動的に以下の機能を提供してくれます:
- ページネーション:
paginate_by = 10
と書くだけ! - 空の時の処理:データがない時も適切に処理
- テンプレート変数:
page_obj
、is_paginated
などを自動で用意
関数ビューだと自分で実装する必要がある機能が、最初から使えるんです!
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
に追加:
urlpatterns = [
# 既存のパターン(省略)...
# アーカイブ
path(
"archive/<int:year>/<int:month>/",
views.MonthArchiveView.as_view(),
name="archive_month",
),
]
▶アーカイブページのスタイルを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
関数に追加します:
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
という見慣れない関数が出てきます。
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)」のようなリンクを表示できるんです!
他のビューにも追加が必要です
実は、post_detail
、category_posts
、MonthArchiveView
にも同じようにarchives
を追加する必要があります。
「えっ、全部に?面倒...」と思いましたか?
その通りです!これが関数ビューの限界なんです。でも安心してください、後でもっと良い方法を紹介していきます!
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/ にアクセスして確認してみましょう:
アーカイブ機能を効果的に確認する方法
アーカイブ機能の動作を確認するには、Admin画面で記事の作成日を調整すると良いでしょう:
- Admin画面(http://localhost:8000/admin/)にログイン
- 「記事」をクリックして記事一覧を表示
- 編集したい記事をクリック
- 「作成日」フィールドを変更(例:2025年6月、7月、8月など異なる月に設定)
- 保存後、各月のアーカイブページ(例:
/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_obj
、is_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が:
- ページネーション処理
- 空のリストの処理
- テンプレートへのデータ受け渡し
- エラーハンドリング
などを自動的に処理してくれます。
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に書き換えて、違いを実感してみましょう。
現在の関数ベースビュー
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版の実装
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)
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で共有可能 |
学習コスト | 低い(関数だけ) | 高い(クラスの概念が必要) |
どっちを使うべき?
FBVがおすすめな場合:
- シンプルな処理
- 特殊なロジックが多い
- チームがCBVに慣れていない
CBVがおすすめな場合:
- 標準的なCRUD操作
- 同じような処理が多い
- 大規模なプロジェクト
僕は最初FBVばかり使っていましたが、CBVの便利さを知ってからは使い分けています!
▶CBV版を試してみる
URLパターンを一時的に変更して試してみましょう:
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
を作成:
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
を修正:
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
Mixinを使う時の注意点
Mixinは必ず左側に書きます:
class AboutView(CategoryListMixin, TemplateView): # 正しい
class AboutView(TemplateView, CategoryListMixin): # 動かない!
これはPythonの仕組みによるものです。左から順番に処理されると覚えておけばOK!
▶他の便利なMixinも作ってみる
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に書き換えてみましょう:
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版を使うようにします:
urlpatterns = [
# FBV版をコメントアウトして、CBV版を有効化
# path("", views.post_list, name="post_list"),
path("", views.PostListView.as_view(), name="post_list"),
# 他のパターンはそのまま...
]
▶テンプレートにページネーションを追加
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 %}
ListViewの自動ページネーション
paginate_by = 10
を設定するだけで、ListViewは自動的に:
- 10件ずつに分割
page_obj
とis_paginated
をテンプレートに渡す- URLパラメータ
?page=2
などを処理
これらすべてを自動で行ってくれます!関数ビューでは自分で実装する必要があった処理が、たった1行で完了です。
これで、記事一覧ページもCBV化が完了しました。SidebarMixin
のおかげで、カテゴリーとアーカイブのデータも自動的に追加されています。
まとめ
今回は、Djangoのクラスベースビュー(CBV)について学びました:
▶できるようになったこと
- TemplateViewでシンプルなページを作成
- ListViewで一覧表示(ページネーション付き)
- DetailViewで詳細表示
- Mixinで共通処理をまとめる
▶解決した問題
冒頭で提示した「同じコードを何度も書く」問題は、MixinとCBVで解決されました:
Before(すべてのビューで):
categories = Category.objects.all().order_by("name")
After(Mixinを使えば):
class MyView(CategoryListMixin, ListView):
# categoriesは自動的に追加される!
▶実践的なアドバイス
-
無理にすべてをCBVにしない
- 既存のFBVが動いているならそのままでもOK
- 新機能から徐々にCBVを使ってみる
-
最初はシンプルに
- まずは基本的な使い方を覚える
- 慣れてきたらMixinを活用
-
適材適所で使い分ける
- シンプルな処理 → 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の活用
- 記事の作成・編集・削除機能の実装
- ユーザー認証と権限管理の基礎
いよいよ、管理画面を使わずに記事を投稿できるようになります。本格的なブログシステムの完成まであと少し!次回もお楽しみに!🚀
この記事はいかがでしたか?
もしこの記事が参考になりましたら、
高評価をいただけると大変嬉しいです!
皆様からの応援が励みになります。ありがとうございます! ✨