【Django】ページネーション機能を実装する|簡単な掲示板アプリの作成
Djangoの標準機能「Paginator」を使い、掲示板にページネーション(ページ送り)機能を実装する方法を解説します。ビューで大量の投稿データをページごとに分割し、テンプレートで「次へ」やページ番号といったリンクを動的に表示する手順を学びます。
開発環境
- OS: Windows10
- Visual Studio Code: 1.73.0
- Python: 3.10.11
- Django: 5.0.3
サンプルコード
/app/views.py
/app/views.py1 from django.core.mail import send_mail, EmailMessage 2 from django.conf import settings 3 from django.template.loader import render_to_string 4+from django.core.paginator import Paginator 5 6 def user_owns_board(view_func): 7 @wraps(view_func) 8 user = request.user 9 10 if user.is_authenticated: 11- boards = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at') 12+ boards_query = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at') 13 else: 14- boards = Board.objects.all().order_by('-updated_at') 15+ boards_query = Board.objects.all().order_by('-updated_at') 16+ 17+ paginator = Paginator(boards_query, 10) 18+ page_number = request.GET.get('page') 19+ boards = paginator.get_page(page_number) 20+ 21 return render(request, 'index.html', {'boards': boards}) 22 23 @login_required 24
/templates/index.html
/templates/index.html1 <a href="{% url 'new' %}" class="btn btn-primary">新規投稿作成</a> 2 <a href="{% url 'my_boards' %}" class="btn btn-primary">自分の投稿一覧</a> 3 </section> 4+ 5+ <!-- ページネーションのリンクを表示 --> 6+ <section class="pagination justify-content-center"> 7+ <ul class="pagination"> 8+ {% if boards.has_previous %} 9+ <li class="page-item"> 10+ <a class="page-link" href="?page=1">« 最初</a> 11+ </li> 12+ <li class="page-item"> 13+ <a class="page-link" href="?page={{ boards.previous_page_number }}">前へ</a> 14+ </li> 15+ {% endif %} 16+ 17+ <li class="page-item disabled"> 18+ <a class="page-link" href="#">ページ {{ boards.number }} / {{ boards.paginator.num_pages }}</a> 19+ </li> 20+ 21+ {% if boards.has_next %} 22+ <li class="page-item"> 23+ <a class="page-link" href="?page={{ boards.next_page_number }}">次へ</a> 24+ </li> 25+ <li class="page-item"> 26+ <a class="page-link" href="?page={{ boards.paginator.num_pages }}">最後 »</a> 27+ </li> 28+ {% endif %} 29+ </ul> 30+ </section> 31 {% endblock %} 32
コード解説
変更点: ビューでPaginatorを使い、投稿一覧をページ分割する
/app/views.py1+from django.core.paginator import Paginator 2 3 def user_owns_board(view_func): 4 @wraps(view_func) 5 user = request.user 6 7 if user.is_authenticated: 8- boards = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at') 9+ boards_query = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at') 10 else: 11- boards = Board.objects.all().order_by('-updated_at') 12+ boards_query = Board.objects.all().order_by('-updated_at') 13+ 14+ paginator = Paginator(boards_query, 10) 15+ page_number = request.GET.get('page') 16+ boards = paginator.get_page(page_number) 17+ 18 return render(request, 'index.html', {'boards': boards})
Djangoに標準で備わっているページネーション機能を使うため、まずdjango.core.paginatorからPaginatorクラスをインポートします。
次に、データベースから取得した全投稿データを、これまでのboardsからboards_queryという変数名に変更しています。これは、ページ分割する「前」の全データであることを分かりやすくするためです。
paginator = Paginator(boards_query, 10)の部分で、Paginatorクラスのインスタンスを生成しています。第1引数にはページ分割したいデータのリスト(今回は全投稿データであるboards_query)、第2引数には1ページあたりに表示したい件数(今回は10件)を指定します。
page_number = request.GET.get('page')では、ユーザーがアクセスしたURLからページ番号を取得しています。例えば、http://example.com/?page=2というURLであれば、page_numberには2が代入されます。.get()を使うことで、URLにpageパラメータが存在しない場合でもエラーにならず、Noneが返ります。
最後に、boards = paginator.get_page(page_number)で、指定されたページ番号に該当する投稿データだけをboards変数に格納しています。このget_pageメソッドは非常に便利で、指定されたページ番号が存在しない場合は自動的に最初のページを、ページ番号が数字でないなどの無効な値の場合は最後のページを返してくれます。このboardsが、最終的にテンプレートへ渡されるデータとなります。
変更点: テンプレートにページネーションのリンクを追加する
/templates/index.html1+ <!-- ページネーションのリンクを表示 --> 2+ <section class="pagination justify-content-center"> 3+ <ul class="pagination"> 4+ {% if boards.has_previous %} 5+ <li class="page-item"> 6+ <a class="page-link" href="?page=1">« 最初</a> 7+ </li> 8+ <li class="page-item"> 9+ <a class="page-link" href="?page={{ boards.previous_page_number }}">前へ</a> 10+ </li> 11+ {% endif %} 12+ 13+ <li class="page-item disabled"> 14+ <a class="page-link" href="#">ページ {{ boards.number }} / {{ boards.paginator.num_pages }}</a> 15+ </li> 16+ 17+ {% if boards.has_next %} 18+ <li class="page-item"> 19+ <a class="page-link" href="?page={{ boards.next_page_number }}">次へ</a> 20+ </li> 21+ <li class="page-item"> 22+ <a class="page-link" href="?page={{ boards.paginator.num_pages }}">最後 »</a> 23+ </li> 24+ {% endif %} 25+ </ul> 26+ </section>
ビューから渡されたboardsオブジェクト(Pageオブジェクト)が持つ情報を使い、ページネーションのリンクを動的に生成します。
{% if boards.has_previous %}は、現在表示しているページよりも「前」のページが存在するかどうかを判定するテンプレートタグです。もし前のページが存在する場合(つまり、2ページ目以降を表示している場合)にのみ、{% endif %}までのHTMLが出力されます。このブロックの中では、「最初」ページへのリンク(?page=1)と、「前へ」のリンク(?page={{ boards.previous_page_number }})を生成しています。boards.previous_page_numberで、現在のページの一つ前のページ番号を取得できます。
中央の<li>タグでは、現在のページ情報を表示しています。{{ boards.number }}で現在のページ番号を、{{ boards.paginator.num_pages }}で全体の総ページ数を取得し、「ページ X / Y」という形式で表示します。
{% if boards.has_next %}は、has_previousとは逆に、「次」のページが存在するかどうかを判定します。もし次のページが存在する場合(最後のページ以外を表示している場合)にのみ、{% endif %}までのHTMLが出力されます。このブロックの中では、「次へ」のリンク(?page={{ boards.next_page_number }})と、「最後」のページへのリンク(?page={{ boards.paginator.num_pages }})を生成しています。boards.next_page_numberで次のページの番号を、boards.paginator.num_pagesで最後のページの番号(=総ページ数)を取得できます。
これらのリンクのhref属性が?page=...という形式になっていることで、views.pyのrequest.GET.get('page')と連携し、ページを移動する機能が実現されます。
おわりに
今回はDjangoの標準機能であるPaginatorを使い、掲示板にページネーション機能を実装する方法を学びました。ビューでは、Paginatorクラスを使って全投稿データを指定した件数で分割し、URLのパラメータに応じて表示するページを決定しました。テンプレート側では、ビューから渡されたオブジェクトが持つhas_nextやnext_page_numberといった属性を利用して、「次へ」やページ番号などのリンクを動的に表示する仕組みを実装しました。