以下の記事はDjangoクラスベースビュー(CBV)探求シリーズの最終回で、ListViewを深く活用してページング検索並び替えなど実務で頻繁に使用される機能を実装する方法を紹介します。前回までの7回ではCBVの基本からMixinの活用まで考察しましたが、今回はそれを総合し、実際のWeb開発で遭遇する一般的な要求をCBVでどのようにクリーンに処理できるかを探ります。

前回のリンクは以下をクリックしてください!

クラスベースビュー(CBV)探求シリーズ ⑦ - Mixinの活用と権限管理


「Django ListViewを拡張して強力なリスト機能を実装し、ユーザー体験を向上させましょう!」


1. ListView、それ以上の可能性

ListViewはデータリストを表示するのに非常に便利なGeneric Viewです。しかし、実際のWebアプリケーションでは単にリストを表示するだけでなく、大量のデータを効率的に管理するためのページング、必要な情報を迅速に見つけるための検索、そしてユーザーの好みに応じた並び替え機能が必須です。

今回の章では、ListViewを拡張してこれらの実務的な要求をどのようにCBVの利点を活用して簡単にエレガントに実装できるかを見ていきます。


2. ページング(Pagination)の適用

大量のデータを一度に表示することはユーザー体験を損ねます。ページングはデータを複数のページに分けて表示することでユーザーの読み込み負担を軽減し、必要な情報をより簡単に探し出すことを助けます。

2.1 基本ページング設定

ListViewは組み込みのページング機能を提供しています。ビュークラスでpaginate_by属性を設定するだけで済みます。

views.pyの例

from django.views.generic import ListView
from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10 # 1ページに10個のArticleオブジェクトを表示
  • paginate_by = 10: このように設定すると、ListViewは自動的に結果を10個ずつ分割してページを構成し、テンプレートコンテキストにページング関連のオブジェクト(paginatorpage_objis_paginated)を渡します。

2.2 テンプレートでのページングUIの実装

テンプレートではコンテキストに渡されたpage_objを使用してページ番号リンクを表示し、前/次ページボタンなどを実装できます。

articles/article_list.htmlの例

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>まだ投稿された記事がありません。</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1">&laquo; 最初</a>
            <a href="?page={{ page_obj.previous_page_number }}>前へ</a>
        {% endif %}

        <span class="current">
            ページ {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}>次へ</a>
            <a href="?page={{ paginator.num_pages }}>最後 &raquo;</a>
        {% endif %}
    </span>
</div>
  • page_obj: 現在のページのデータを含むオブジェクトです。has_previoushas_nextprevious_page_numbernext_page_numbernumberなどの属性を提供します。
  • paginator: 全ページに関する情報を含むオブジェクトです。num_pages(総ページ数)などの属性を持っています。

2.3 URL接続維持

ページングリンクを作成する際に?page=2のようにpageクエリパラメータを使用しているのが見受けられます。ListViewはこのパラメータを自動的に認識し、そのページのデータを表示します。


ウェブサイトで必要な情報を迅速に見つけることは非常に重要です。ListViewを拡張して検索機能を実装してみましょう。

3.1 フォーム(Form)の定義

まず、検索語を入力するための簡単なフォームを定義します。

forms.pyの例

from django import forms

class ArticleSearchForm(forms.Form):
    search_term = forms.CharField(label='検索語', required=False)

3.2 ListViewの修正

ListViewget_queryset()メソッドをオーバーライドして検索機能を実装します。

views.pyの例

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET) # GETリクエストからフォームデータを取得

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                # タイトルや内容に検索語が含まれるArticleをフィルタリング
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form # テンプレートにフォームを渡す
        return context
  • get_queryset(): このメソッドをオーバーライドして基本のクエリセットを修正します。
  • フォームインスタンスを生成し、有効性検査を通過すればcleaned_dataから検索語を取得します。
  • Qオブジェクトを使用して、タイトル(title)または内容(content)に検索語が含まれるArticleオブジェクトをフィルタリングします(__icontainsは大文字小文字を無視して含まれるかどうかを確認します)。
  • get_context_data(): テンプレートでフォームをレンダリングできるようにフォームインスタンスをコンテキストに追加します。

3.3 テンプレートでの検索フォーム表示と結果保持

テンプレートに検索フォームを追加し、検索結果を保持しつつページングが可能なようにURLを構成します。

articles/article_list.html

<form method="get">
    {{ search_form }}
    <button type="submit">検索</button>
</form>

<ul>
    {% for article in articles %}
        <li>{{ article.title }} - {{ article.created_at }}</li>
    {% empty %}
        <li>検索結果がありません。</li>
    {% endfor %}
</ul>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}">&laquo; 最初</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">前へ</a>
        {% endif %}

        <span class="current">
            ページ {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}">次へ</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}">最後 &raquo;</a>
        {% endif %}
    </span>
</div>
  • フォームのmethodgetに設定し、検索語をURLクエリパラメータとして渡します。
  • ページリンクを作成する際に現在の検索語(search_form.search_term.value)をURLパラメータに含め、検索結果を保持しながらページングを行います。|default:''フィルターを使用して、検索語がない場合には空の文字列を使用します。

ListView画面例 – 検索と並び替えUIを含むリストページ


4. 並び替え(Ordering)機能の追加

ユーザーがデータを希望の基準で並び替えられるように並び替え機能を追加してみましょう。

4.1 並び替え選択フォームの追加 (オプション)

並び替え基準を選べるドロップダウンフォームを追加することもできます。単純にURLパラメータだけで処理できる場合もあります。ここではURLパラメータを使用する方法を説明します。

4.2 ListViewの修正

get_queryset()メソッドを修正してorder_by()を適用します。

views.pyの例(検索機能含む)

from django.views.generic import ListView
from django.db.models import Q
from .models import Article
from .forms import ArticleSearchForm

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)
        ordering = self.request.GET.get('ordering', '-created_at') # 基本の並び順: 最新順

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )

        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = self.form
        context['ordering'] = self.request.GET.get('ordering', '-created_at') # 現在の並び順基準をテンプレートに渡す
        return context
  • self.request.GET.get('ordering', '-created_at'): URLクエリパラメータからorderingの値を取得します。値がない場合はデフォルトで-created_at(最新順)に並び替えます。
  • queryset = queryset.order_by(ordering): 取得したorderingの値を使用してクエリセットを並び替えます。モデルフィールド名の前に-を付けると逆順になります。

4.3 テンプレートでの並び替えリンクの提供

テンプレートに各フィールドごとに並び替えるためのリンクを提供します。

articles/article_list.htmlの例

<form method="get">
    {{ search_form }}
    <button type="submit">検索</button>
</form>

<table>
    <thead>
        <tr>
            <th><a href="?ordering=title&search_term={{ search_form.search_term.value|default:'' }}">タイトル</a></th>
            <th><a href="?ordering=-created_at&search_term={{ search_form.search_term.value|default:'' }}">作成日</a></th>
            </tr>
    </thead>
    <tbody>
        {% for article in articles %}
            <tr>
                <td>{{ article.title }}</td>
                <td>{{ article.created_at }}</td>
            </tr>
        {% empty %}
            <tr><td colspan="2">検索結果がありません。</td></tr>
        {% endfor %}
    </tbody>
</table>

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?page=1&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">&laquo; 最初</a>
            <a href="?page={{ page_obj.previous_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">前へ</a>
        {% endif %}

        <span class="current">
            ページ {{ page_obj.number }} / {{ paginator.num_pages }}
        </span>

        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">次へ</a>
            <a href="?page={{ paginator.num_pages }}&search_term={{ search_form.search_term.value|default:'' }}&ordering={{ ordering }}">最後 &raquo;</a>
        {% endif %}
    </span>
</div>
  • 各テーブルヘッダーに並び替え基準(ordering)パラメータを含むリンクを提供します。
  • ページリンクを生成する際に現在の検索語だけでなく、現在の並び替え基準(ordering)もURLパラメータに含めて並び替え状態を保持しつつページングを行えるようにします。

検索/並び替えプロセスダイアグラム – ListView拡張フローの視覚化


5. Mixinで機能を結合する

以前に学んだMixinを活用することでページング、検索、並び替え機能をさらにスッキリ管理できます。例えば、検索機能をMixinとして分離できます。

mixins.pyの例

from django.db.models import Q
from .forms import ArticleSearchForm

class ArticleSearchMixin:
    def get_queryset(self):
        queryset = super().get_queryset()
        self.form = ArticleSearchForm(self.request.GET)

        if self.form.is_valid():
            search_term = self.form.cleaned_data.get('search_term')
            if search_term:
                queryset = queryset.filter(
                    Q(title__icontains=search_term) | Q(content__icontains=search_term)
                )
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_form'] = getattr(self, 'form', ArticleSearchForm()) # フォームがない場合は基本のフォーム使用
        return context

views.pyの例(Mixin適用)

from django.views.generic import ListView
from .models import Article
from .mixins import ArticleSearchMixin

class ArticleListView(ArticleSearchMixin, ListView):
    model = Article
    template_name = 'articles/article_list.html'
    context_object_name = 'articles'
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset()
        ordering = self.request.GET.get('ordering', '-created_at')
        queryset = queryset.order_by(ordering)
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['ordering'] = self.request.GET.get('ordering', '-created_at')
        return context
  • ArticleSearchMixinを作成し、検索関連のロジックを分離しました。
  • ArticleListViewArticleSearchMixinListViewを継承し、検索機能とリスト機能の両方を持ちます。

6. CBVの利点と限界(シリーズのまとめ)

8回にわたるCBV探求シリーズを通じて、私たちは関数ベースのビュー(FBV)からクラスベースのビュー(CBV)への転換理由、CBVの基本的な構造と活用法、様々なGeneric Viewの使用方法、そしてMixinを利用した機能拡張についての重要な内容を振り返りました。

CBVの主な利点:

  • コード再利用性: Generic ViewとMixinを利用して繰り返しのコードを減らし、効率的な開発が可能です。
  • 構造的なコード: クラスベースのコードはロジックを明確に分離し、管理が容易です。
  • 拡張性: 継承とMixinを通じて既存の機能を簡単に拡張し、新しい機能を追加できます。
  • 保守性: コードがモジュール化されているため、保守が容易で、変更が全コードに影響する可能性を減らします。
  • 開発生産性向上: Djangoが提供する多様なGeneric ViewやMixinを活用して迅速に開発できます。

CBVの限界や考慮事項:

  • 初期学習曲線: FBVに比べてクラスベースの構造を理解する必要があります。
  • 過度の継承: Mixinを多く使用しすぎるとコードの流れを把握しにくくなる可能性があります。適切なレベルのMixin活用が重要です。
  • 単純なロジックにはオーバーエンジニアリング: とても簡単なページや機能の場合、FBVがより簡潔かもしれません。状況に応じて適切なビュー方法を選ぶことが推奨されます。

結論として、Django CBVは複雑で多様な機能を要求するWebアプリケーションを開発するのに非常に強力で便利なツールです。CBVの利点を理解し、適切に活用することで、さらに効率的で保守が容易なコードを作成することができるでしょう。

ここまでクラスベースビュー(CBV)探求シリーズをお読みいただきありがとうございました!このシリーズがDjango CBVを理解し、実際のプロジェクトに応用するのに少しでも役立てていただければ幸いです。


前回の記事を振り返る

  1. クラスベースビュー(CBV)探求シリーズ #1 – FBVからCBVへの理由と開発者の姿勢
  2. クラスベースビュー(CBV)探求シリーズ #2 – Djangoの基本Viewクラスを理解する
  3. クラスベースビュー(CBV)探求シリーズ #3 – FormViewでフォーム処理を簡単にする
  4. クラスベースビュー(CBV)探求シリーズ #4 – ListView & DetailViewの活用法
  5. クラスベースビュー(CBV)探求シリーズ #5 – CreateView、UpdateView、DeleteViewでCRUDを実装する
  6. クラスベースビュー(CBV)探求シリーズ #6 – TemplateView & RedirectViewの活用法
  7. クラスベースビュー(CBV)探求シリーズ #7 – Mixinの活用と権限管理

「ListViewの拡張を通じてより豊かでユーザーフレンドリーなリスト機能を実装し、CBVをマスターしてDjangoの開発スキルをアップグレードしましょう!」