【Django入門13】ページネーションの基本

Djangoにおけるページネーションの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django におけるページネーションについて解説していきます。

ページネーションとは

ウェブアプリにおけるページネーションとは、簡単に言えば1つのページを複数ページに分割して表示することになります。もう少し詳しく言えば、一覧表・一覧リストなどで表示するオブジェクトが多い場合、それを複数のページに分割して表示し、さらに各ページへのリンクを設置することになります。

例えば、Google の検索結果はページネーションの例の1つとなります。Google 検索でキーワードを入力すれば大量の検索結果が得られることになります。ですが、1つのページに全ての結果が表示されるのではなく、複数のページに分割して検索結果が表示されるようになっています。また、検索結果の他のページに簡単に遷移できるよう、他のページへのリンクも設置されています。これにより、検索結果が見やすくなってユーザーにとって使いやすいウェブアプリが実現されています。

このように、量の多いオブジェクトを複数のページに分割して表示し、さらに他のページへのリンクを設置することをページネーションと呼びます。

ページネーションの説明図

ちなみに、前回と前々回の連載の中で管理画面について説明しましたが、実はこの管理画面でもページネーションが利用されています。

ウェブアプリでは、特にデータベースに保存されているレコードの一覧を表示するような際にページネーションが利用されることが多いです。下記ページでも解説しているとおり、Django においてレコードとは「モデルクラスのインスタンス」のことになりますので、つまりは Django におけるページネーションとはモデルクラスのインスタンスの一覧表・一覧リストを分割して表示する機能と考えて良いです(モデルクラスは開発者自身が定義するものであり、このモデルクラスの定義の仕方によって、様々な一覧表・一覧リストの表示が可能です)。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

このページネーションを実現する上では、まずページの分割が必要になります。また、複数のページに分割されるため、前後のページへの遷移を行うためのリンクの設置なども必要になります。そして、これらを実現するために利用するクラスが、Django フレームワークに用意された PaginatorPage になります。

ページネーションの実現方法

次は、ページネーションの実現方法・実現手順について解説していきます。

Django においては、前述の通りページネーションは Paginator というクラスと Page というクラスの2つを利用して実現することになります。

MEMO

Paginator のスペルに注意してください

4文字目は e ではなく i となっています

Page の4文字目は e となりますが、PaginatorPagination の場合は4文字目が i となります

簡単に言えば、Paginatorページの分割の実行分割後の全ページの管理を行うクラスになります。そして、Page は分割後の1つのページを管理するクラスになります。Page からは前後のページ等の情報が取得できるため、この取得した情報を利用すれば前後のページへのリンクの設置なども簡単に実現できます。

スポンサーリンク

ページネーションを実現する流れ

続いて、ページネーションを実現する流れを解説していきます。ここでは、ページネーションを利用しない場合の「インスタンスの一覧表」を表示する時の処理の流れについて説明した後、それと対比する形で、ページネーションを利用する場合の「インスタンスの一覧表」を表示する時の処理の流れについて説明していきたいと思います。

まず、ページネーションの利用の有無に関わらず、インスタンスの一覧表の表示を実現するために必要になるのが、「コンテキストとして集合を受け取り、その集合に含まれる各インスタンスの情報を出力できるように作成したテンプレートファイル」になります。簡単になりますが、そのようなテンプレートファイルの例の1つが下記で、コンテキストとして集合 objects を受け取れば、テンプレートタグ for によって objects に含まれる各インスタンスのデータ属性の値が出力されるようになっています。

一覧表表示用のテンプレートファイル
<table>
<tbody>
    {% for object in objects %}
    <tr>
        <td>{{ object.データ属性1 }}</td>
        <td>{{ object.データ属性2 }}</td>
    </tr>
    {% endfor %}
</tbody>
</table>

このテンプレートファイルのポイントは、コンテキストとして渡される objects の集合に含まれるインスタンスによってページへの表示内容が変化するという点になります。この objects にデータベースから取得した全てのインスタンスを含ませておけば全インスタンスがページに表示されることになりますが、全てのインスタンスではなく、特定のページに割り付けられたインスタンスのみを objects に含ませておけば、ページ分割後のインスタンス一覧の表示が実現できることになります。

テンプレートファイルに渡すデータに応じてページの表示結果が変化する様子

つまり、ページを分割することに限れば、今まで使用してきたような「インスタンスの一覧を表示するテンプレートファイル」と同じものが利用可能ということになります。そして、同じテンプレートファイルを利用して、コンテキストとしてテンプレートファイルに渡す集合を「特定のページに割り付けられたインスタンスのみ」とすれば、とりあえずページ分割後のページ表示は実現できます。ここからページネーションの利用の仕方について解説していきますが、まずは、この集合の作成の仕方に着目しながら解説を読み進めていただければと思います。

また、上記で説明したような「テンプレートファイル」についての解説に関しては下記ページで行っていますので、テンプレートファイルについて学びたい方は下記ページを参照していただければと思います。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレート(Template)の基本
MEMO

コンテキストは、正確にはテンプレートファイルに渡すのではなく render 関数に渡すという表現が正しいです

render 関数にコンテキストとテンプレートファイルパスを引数で渡すことで、テンプレートファイルの各変数参照部分が、コンテキストのキーの値に置き換えられることになります

が、このページでは、説明を簡潔にするため、コンテキストをテンプレートファイルに渡すという表現を用いていますので、この点はご了承ください

ページネーションを利用しない場合の処理の流れ

最初に、おさらいの意味で、ページネーションを利用しない場合の「インスタンスの一覧表」を表示する時の処理の流れについて解説していきます。ページネーションを利用しない「インスタンスの一覧表」の表示は、下の図のような処理の流れで実現できます。

ページネーションを利用しない場合のインスタンス一覧表示時の処理の流れ

Django でウェブアプリを開発する場合、処理の流れは基本的にビューに実装していくことになります。なので、ここからの説明は主にビューに関する説明になります。

処理の流れについて説明していくと、まずビューは、一覧表に表示したいモデルクラスのインスタンスの集合をモデルを利用してデータベースから取得します。そして、取得した集合をそのままコンテキストとしてテンプレートファイルに渡します。これによって「インスタンス一覧表」の HTML が生成できることになります(実際には、HTML の生成は render 関数で実施されます)。

ただし、この場合、データベースから取得した集合がそのままコンテキストとしてテンプレートファイルに渡されることになるため、集合に含まれるインスタンスの数がどれだけ多くても、1つのページに全てのインスタンスが表示されることになります。

ページネーションを利用する場合の処理の流れ

それに対し、ページネーションを利用する場合の処理の流れは下の図のようになります。

ページネーションを利用する場合のインスタンス一覧表示時の処理の流れ

まず、モデルを利用してデータベースからモデルクラスのインスタンスの集合を取得するという点は同じです。ですが、ページネーションを利用する場合、その集合をテンプレートファイルに渡すのではなく Paginator に渡します。これにより、Paginator によって複数のページ(Page)が生成され、集合に含まれるインスタンスが各ページに割り付けられることになります。元々は1つだった集合が複数分のページに割り付けられることになるため、つまりはページの分割が行われることになります。

Paginatorによってページ分割が行われる様子

この、Paginator によって生成された複数の PagePaginator によって管理されることになり、ビューから Paginator に対してページ番号を指定することで、そのページに対応する Page を取得することができます。なので、ビューはページ分割実施後、表示したいページのページ番号を指定して Paginator から、その指定したページ番号に対応する Page を取得する必要があります。

ページ番号を指定してPaginatorからPageを取得する様子

ここで取得する Page はイテラブルなオブジェクトであり、リストや集合等と同様に for ループ等が実行可能です。なので、ビューからテンプレートファイルにコンテキストとして Page を渡すことで、ページネーションを利用しない場合と同様に「インスタンス一覧表」の HTML が生成できることになります。ただし、Page に対する for ループでは、その Page に割り付けられたインスタンスに対してのみループが行われることになりますので、ここで生成される HTML は、そのページに割り付けられたインスタンスの情報のみが表示される HTML となります。

Pageをコンテキストとしてテンプレートファイルに渡すことでページ分割後のページのHTML生成が実現できることを示す図

つまり、この HTML によって表示されるページは、もともとのインスタンスの一覧表を分割したものとなります。後は、上記の処理の流れの中で、ユーザーから指定されたページ番号の PagePaginator から取得するようにすれば、そのページ番号に応じた分割後のページが表示できるようになります(後述で示すように、このページ番号は URL のクエリパラメーターによって指定されます)。

指定したページ番号に応じた分割後のページが表示される様子

ですが、上記で実現できるのはページの分割だけです。

ページの分割を行うのであれば、他のページに遷移するためのリンクを設置したり、表示中のページのページ番号等を表示したりしてあげた方が親切です。特に、他のページへのリンクが無いと、単に表示されるインスタンスの数が減っただけで不便になってしまいます。ということで、ページネーションでは、ページ分割だけでなく、こういった使い勝手を向上させるためのリンクや情報の出力もセットで行う必要があります。

ただ、Page からは、そのページや前後のページの情報が取得できるため、ビューからテンプレートファイルに Page さえ渡してやれば、後は、そういった情報が出力できるようにテンプレートファイルを作成するだけで、上記のような使い勝手を向上させるための出力が実現できることになります。つまり、ビューが実行する処理の流れに関しては、これまで説明した内容から変更する必要が無く、テンプレートファイルの作りを変更してやれば良いだけです。

他のページへのリンクや必要な情報が出力できるようにテンプレートファイルを作成する必要があることを示す図

どういった情報をどういった方法で出力すればよいのか?という点については後述で解説しますので、まずは、ページネーションを利用するためには上記で説明したような処理の流れが必要であるということを覚えておいてください。

また、ここまでの説明から分かるように、ページネーションを利用する場合、ページネーションを利用していなかったときに比べて、ビューやテンプレートから PaginatorPage を利用して処理を行うところが新たに実装が必要になる部分となりますので、ここからはこれらの処理に焦点を当てて解説を行なっていきます。具体的には下記の3つについて説明していきます。

  • ページを分割する
  • 特定のページを取得する
  • ページの情報を取得する
MEMO

このページでは、上記で示した流れに基づいて解説を行なっていきますが、もっと別の実装でもページネーションを実現することは可能です

例えば、ビューから Page の情報を取得し、その取得したデータをテンプレートに渡してやるような実装もあり得ます

ページを分割する

まずは、ページを分割する処理について解説していきます。前述の通り、このページの分割はビューから Paginator を利用して実現していくことになります。

Paginator のコンストラクタの実行

Paginator を利用する際には、まず Paginator のコンストラクタを実行してインスタンスの生成を行います。

この Paginator は Django フレームワークの django.core.paginator で定義されているクラスなので、Paginator のコンストラクタを実行するためには、django.core.paginator から Paginatorimport しておく必要があります。

また、Paginator のコンストラクタには、下記のように4つの引数を指定することが可能であり、object_listper_page の引数指定は必須となります。

コンストラクタの実行
from django.core.paginator import Paginator
paginator = Paginator(object_list, per_page, orphans, allow_empty_first_page)

object_list 引数(第1引数)には表示対象の全てのモデルのインスタンスの集合を指定します。基本的には、データベースから取得したクエリーセットを指定することになると思います。後述の インスタンスの集合の整列 で説明するように、この引数に指定する集合は事前に整列させておく必要がある点に注意してください。

さらに、per_page 引数(第2引数)には1つのページに表示したいインスタンスの個数を整数で指定します。

これらを指定して Paginator のコンストラクタを実行すれば、object_list で与えられたクエリーセットの要素が per_page 個ごとに分割され、別々のページに割り付けられることになります。

Paginatorによるページ分割の説明図

例えば、object_list 引数に指定したクエリーセットのインスタンスの数が 32per_page10 を指定した場合、下記のように各ページにインスタンスが分割されて割り付けられることになります。

  • 1ページ目:object_list[0:10] (09 個目のインスタンス)
  • 2ページ目:object_list[10:20] (1019 個目のインスタンス)
  • 3ページ目:object_list[20:30] (2029 個目のインスタンス)
  • 4ページ目:object_list[30:32] (3031 個目のインスタンス)

このように、Paginator のコンストラクタを実行すれば、本来1ページに全て表示されていた object_list が複数のページに割り付けられることになります。つまり、Paginator のコンストラクタの実行によりページの分割が行われることになります。そして、返却値として得られる Paginator のインスタンスが、その分割後のページの情報を管理することになります。

インスタンスの集合の整列

ということで、Paginator のコンストラクタを実行することでページの分割が行われることになるのですが、object_list 引数に指定するインスタンスの集合は明示的に整列させておく必要があるので注意してください。

整列させていない状態のインスタンスの集合を object_list 引数に指定すると、Paginator のコンストラクタ実行時に下記のような警告が標準出力に出力されることになります。

UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: <class 'django.contrib.auth.models.User'> QuerySet.        
  paginator = Paginator(users, 2)

データベースから取得したインスタンスの集合(クエリーセット)には order_by メソッドが用意されており、このメソッドを利用することで明示的にインスタンスの集合を整列させることができます。より具体的には、インスタンスの集合に order_by('フィールド名') を実行させることで、インスタンスの集合が フィールド名 に対して昇順に整列することになります('-フィールド名' を指定することで フィールド名 に対して降順に整列させることもできます)。

例えば下記は、User というモデルクラスのテーブルの全インスタンスを含む集合 usersobject_list としてページ分割を行う例となりますが、この場合、インスタンスの集合が整列されていないため、前述のような警告が発生することになります。

未整列の集合に対するページ分割
users = User.objects.all()
paginator = Paginator(users, 2)

それに対し、下記の場合は order_by によって整列されたインスタンスの集合 users が object_list 引数に指定されているため、警告の発生無しにページの分割が行われることになります(User には date_joined フィールドが必要です)。

未整列の集合に対するページ分割
users = User.objects.all().order_by('-date_joined')
paginator = Paginator(users, 2)

ということで、ページ分割を行う際は、インスタンスの集合を明示的に整列しておく必要がある点に注意してください。

orphans 引数

さて、先ほど Paginator のコンストラクタの引数には object_listper_page のみを指定しましたが、他にも orphans 引数と allow_empty_first_page 引数も指定可能ですので、これらの引数について説明をしておきます。

MEMO

ただ、これらの引数はページネーションの実現においては重要度も低く、デフォルトの設定でも問題ないため、ページを取得する まで読み飛ばしていただいても問題ありません

1つ目の orphans 引数はちょっとややこしいです。まず orphans は孤児という意味の単語であり、orphans 引数で指定した整数以下の個数のインスタンスは孤児であるとみなされ、これらのインスタンスは他のページに合流させて表示されることになります。

もう少し具体的に説明すると、ページの分割を行うと最後のページだけ表示されるインスタンスの数が中途半端になることがあります。例えば、前述の例では最後のページに 2 つのインスタンスのみが表示されることになります。こういった最後のページに表示されるインスタンスの数が少ない場合、つまり孤児であるとみなされる場合に、それらのインスタンスを前のページにまとめて表示させるというのが orphans 引数の効果となります。

orphans引数の説明図

前述の例の場合、orphans 引数に 2 を指定しておけば、ページに表示されるインスタンスの数が 2 以下の場合に、それらのインスタンスは前のページに含めて表示されることになります。つまり、下記のようにページの表示が行われることになります。

  • 1ページ目:object_list[0:10] (09 個目のインスタンス)
  • 2ページ目:object_list[10:20] (1019 個目のインスタンス)
  • 3ページ目:object_list[20:32] (2031 個目のインスタンス)

orphans 引数を指定しなかった場合、orphans にはデフォルト値の 0 が設定されることになります。つまり、最後のページに表示されるインスタンスが 1 であったとしても、そのインスタンスは個別に最後のページに表示されることになります。

allow_empty_first_page 引数

allow_empty_first_page 引数は、その名の通り、最初のページに表示するインスタンスの数が 0 個であることを許可する(True)or 許可しない(False)を指定する引数になります。デフォルトは True です。

False を指定した場合、最初のページに表示するインスタンスの数が 0 個である場合に下記のような例外が発生することになります。

That page contains no results

ウェブアプリの公開初期や開発段階などの場合、表示するインスタンスの数が 0 であることも多いと思いますので、何らかの理由があって例外を発生させたい場合以外は、基本的には True を指定しておけば良いと思います。

ページを取得する

前述の通り、Paginator のコンストラクタを実行することでページの分割が行われることになります。

ページの分割後に行うことは、分割後のページの取得になります。このページの取得もビューから Paginator を利用して実現していくことになります。

ページの取得

このページの取得は Paginator のインスタンスに page メソッドを実行させることで実現できます。page メソッドの引数 num_page には、取得したいページのページ番号を整数で指定します。ページ番号は 1 から始まるという点に注意してください。

ページの取得
page_obj = paginator.page(num_page)

page メソッドの返却値は Page というクラスのインスタンスとなり、このインスタンスは num_page で指定されたページの情報をデータ属性で持っていたり、ページの情報を取得するメソッドを持っていたりします。

ページ番号の取得

上記のように、ページを取得すること自体はページ番号を指定して page メソッドを実行すれば良いだけなので簡単に思えます。

ですが、実は page メソッドの引数に指定するページ番号を取得するのに少し工夫が必要となります。例えば、下の図のようなページ表示の場合、現在2ページ目を表示しているので、次へ リンクがクリックされた際には3ページ目を表示する必要があります。

次へのリンクをクリックされた時に設定するURLの説明図

つまり、次へ リンクがクリックされた際には、次のページの表示を行うために page メソッドの引数にページ番号として 3 を指定する必要があります。このページ番号はどのようにして取得すれば良いでしょうか?

やり方はいろいろあって、URL パターンの仕組みを利用することもできるのですが、このページネーションを実現する際にはクエリパラメーターが利用されることが多いです。クエリパラメーターとは、URL の最後部分に下記の形式で指定されるパラメーターになります。

クエリパラメーター
?変数名 = 値

そして、このクエリパラメーターはビューから取得することが可能です。具体的には、下記のように 、ビューの関数の第1引数 request のデータ属性から取得することが可能です。

クエリパラメーターの取得
number = int(request.GET.get('変数名', 1))

get の第2引数には 変数名 のクエリパラメーターが指定されていない場合のデフォルト値を指定します。つまり、上記の場合、クエリパラメーターが指定されなかった場合は number1 がセットされることになります。クエリパラメーターが必ず指定されるとは限らないため、このデフォルト値は指定するようにしたほうが良いです。

ということで、上記のとおり、ビューからはクエリパラメーターに指定された値を取得可能ですので、先ほど示した 次へ 等の「他のページへの遷移を行うリンク」の URL の最後にクエリパラーメータを付加してやれば、ビューから次に表示すべきページ番号を取得することができることになります。そして、ページ番号が取得できれば、そのページの Pagepage メソッドで取得可能です。

具体的には、例えば変数名 p のクエリパラメーターで次に表示すべきページ番号が指定されるのであれば、下記のような処理によって、そのページの Page インスタンスを取得することができることになります。

表示すべきページのPageインスタンスの取得
number = int(request.GET.get('p', 1))
page_obj = paginator.page(num_page)

上記では変数名を p にしていますが、この部分はページ番号を表すことが分かりやすければ何でも良いです。

クエリパラメーターでのページ番号の指定

ここまでの説明のとおり、クエリパラメーターでページ番号が指定されるようになりさえすれば、次に表示すべきページの Page のインスタンスは取得可能です。

ただ、この「クエリパラメーターでのページ番号の指定」に関しては、どのようにして実現すればよいでしょうか?

これに関しては、テンプレートファイルの作り方を工夫することで実現可能です。

Django で開発したウェブアプリのページは、基本的にはテンプレートファイルから生成されることになります。そして、ページに表示されるリンクもテンプレートファイルによって出力されることになります。さらに、このリンクには、クリックされた際に送信するリクエストの URL を指定することができます。ですので、その URL に、表示するページのページ番号を指定するクエリパラメーターを追加するようにしてやれば、クエリパラメーターでのページ番号の指定が実現できることになります。

具体的には、HTML ではリンクの要素は a タグによって出力することができ、さらにリンク先の URL は href で指定できるため、下記のようなタグをテンプレートファイルに記述しておけば、next というリンクをクリックした時に相対パス指定で ?p=3 をクエリパラメーターとした URL のリクエストを送信できるようになります。

クエリパラメーターの付加
<a href="?p=3">next</a>

ただ、上記の 3 のように固定でページ番号を指定するのではなく、表示中のページに合わせて動的にページ番号が変化するようにテンプレートファイルを作成する必要があります。例えば、今 n ページ目を表示しているのであれば、next のリンクをクリックしたときにはクエリパラメーターとして ?p=n+1 が指定されるようにする必要があります。このような動的なページ番号の指定に関しては Page のインスタンスの利用によって実現が可能で、これに関しては次の節で解説していきます。

MEMO

href="?p=3"?p=3 は相対パス指定となるため、今表示しているページの URL の最後にクエリパラメーターが追加されたリクエストが送信されることになります

href="/?p=3" と最初に / を付けるとルートパス指定になり、URL 自体が変化してしまうことになるので注意してください

前者の場合、例えば表示しているページの URL が http://localhost:8000/appli/ である場合、リンクのクリックによって送信されるリクエストの URL はクエリパラメーターを含めて http://localhost:8000/appli/?p=3 となります

それに対し、後者の場合は、http://localhost:8000/?p=3 となり、元々 URL に含まれていた /appli の部分が消えてしまうことになります

スポンサーリンク

ページの情報を取得する

ここまでの説明により、ページの分割および、表示すべきページの Page のインスタンスの取得が実現できるようになったことになります。Page のインスタンスが取得できれば、後は、その Page のインスタンスからデータ・情報を取得して、あなたの実現したいウェブアプリの動作やページの表示が行えるよう実装していけばよいことになります。

ただ、この Page のインスタンスからは、どんなデータ・情報が取得できるのでしょうか?また、それらのデータ・情報はどうやって取得すればよいのでしょうか?

ここからは、これらの点についての詳細を解説していきます。

Page に割り付けられたインスタンスの取得

まず、Page のインスタンスからは、そのページに割り付けられたモデルクラスのインスタンスを取得することが可能です。Page のインスタンスはイテラブルなオブジェクトであり、下記のように for ループでページに割り付けられたモデルクラスのインスタンスを1つ1つ取得することが可能となります。

モデルクラスのインスタンスの取得
# page_obj : Pageのインスタンス
for obj in page_obj:
        print(obj)

また、テンプレートファイルでも、for タグを利用して同様のことが実現できます。

テンプレートでのインスタンスの取得
{% for obj in page_obj %}
{{ obj }}
{% endofor %}

Page のデータ属性・メソッド

また、Page のインスタンスからはデータ属性やメソッドによってページの情報を取得することができます。例えばデータ属性 number からは、そのインスタンスに対応するページ番号を取得することができます。

さらに、Page には下記のようなメソッドが存在し、これらのメソッドによって様々なページの情報を取得することができます。下記で示したメソッドは全て「引数なし」で実行することができます。

  • start_index:そのページで表示する最初のインスタンスのインデックスを整数で取得
  • end_index:そのページで表示する最後のインスタンスのインデックスを整数で取得
  • has_other_pages:そのページ以外にページが存在するかどうかをブールで取得
  • has_previous:そのページの前のページが存在するかどうかをブールで取得
  • has_next:そのページの次のページが存在するかどうかをブールで取得
  • previous_page_number:そのページの前のページのページ番号を整数で取得
  • next_page_number:そのページの次のページのページ番号を整数で取得

例えば下記を実行すれば、前のページが存在する場合のみ前ページのページ番号が出力され、次のページが存在する場合のみ次ページのページ番号が出力されることになります。

前・次のページの番号の表示
# page_obj : Pageのインスタンス
if page_obj.has_previous():
    print(page_obj.previous_page_number())
if page_obj.has_next():
    print(page_obj.next_page_number())

また、テンプレートファイルでも、Page のインスタンスのデータ属性やメソッドを利用して同様のことが実現できます。テンプレートファイルでは変数を参照することが可能ですが、その変数が呼び出し可能なオブジェクトである場合、そのオブジェクトが引数なしで実行されることになります。そして、変数部分がそのオブジェクトの実行結果に置き換えられることになります。

この呼び出し可能なオブジェクトにはメソッドも含まれます。また、前述で示したメソッドは全て引数なしで実行可能ですので、上記と同様の出力は、テンプレートファイルに下記のような記述を行うことで実現することができることになります。

テンプレートでの前・次のページの番号の表示
{% if page_obj.has_previous %}
{{ page_obj.previous_page_number }}
{% endif %}
{% if page_obj.has_next %}
{{ page_obj.next_page_number }}
{% endif %}

Paginator のデータ属性・メソッド

また、Page にはデータ属性 paginator が存在し、この paginator は、その Page を生成した Paginator のインスタンスを参照しています。そのため、この paginator から Paginator のデータ属性やメソッドを利用することも可能です。よく使うのが num_pages で、これにより分割後の総ページ数を取得することができます。

総ページ数の取得
# page_obj : Pageのインスタンス
print(page_obj.paginator.num_pages)

当然、テンプレートファイルでも Page のインスタンスのデータ属性 paginator から Paginator のデータ属性やメソッドが利用可能で、例えば下記を実行すれば、分割後の総ページ数の出力が可能となります。

テンプレートでの総ページ数の取得
{{ page_obj.paginator.num_pages }}

Page からは「表示中のページ」や「表示中のページの前後のページ」に関する情報が、Paginator からは「全ページ」や「分割前のインスタンス」に関する情報が取得できますので、これらの情報を利用することで、様々なページ表示や機能が実現可能です。

テンプレートから Page を利用する

ここまでの説明からも分かるように、ページの情報は通常の Python スクリプトでも取得可能ですし、テンプレートファイルからも取得可能です。これらの情報は Page のインスタンスから取得可能ですので、情報をページに出力するだけであれば、ビューからはコンテキストとして Page のインスタンスそのものを渡し、テンプレートファイル側でデータ属性やメソッドを利用して必要な情報を出力することが多いと思います。

Pageからの情報の取得はテンプレートファイルから行うことを示す図

例えば、下の図のようなページを表示することを考えてみましょう。このページでは、モデルクラスのインスタンスのリスト・表示されているページのページ番号(2)、全ページ数(5)、前のページのページ番号(1)、次のページのページ番号(3)が表示されています。

テンプレートファイルからのページの情報の取得例1

このような表示であれば、テンプレートに渡すコンテキストのデータは Page のインスタンスのみで十分実現可能です。前述の通り、Page のインスタンスからは、そのページに割り付けられているモデルクラスのインスタンスを取得して出力することができますし、前後のページ番号もメソッドから取得可能です。さらに、Page のインスタンスのデータ属性 paginator から、分割後の総ページ数等も取得可能です。

例えば、テンプレートファイルが受け取る Page のインスタンスを page_obj とすれば、各項目は下の図のようなテンプレートのタグや変数表示の仕組みを利用して出力することができます。全ての情報が page_obj から取得できていることが確認できると思います。

テンプレートファイルからのページの情報の取得例2

このように、テンプレートファイルで Page のインスタンスの情報を表示することで、モデルのインスタンスの一覧だけでなく、前後のページへ誘導するためのリンク等も設置することができるようになります。

ページネーションの利用例

続いて、ここまで説明してきた内容を踏まえながら実際にページネーションを利用するアプリを開発していきたいと思います。ここでは、下記ページの User クラスとの使い分け で紹介した User のインスタンス一覧を表示するアプリを開発していきます。

【Django入門9】カスタムユーザーによるユーザー管理

まずは、ページネーションを利用しない例を示し、それをページネーションを利用するアプリに変更していきたいと思います。

ページネーションを利用しないアプリ

前述のとおり、最初にページネーションを利用しないアプリを開発していきます。これに関しては、いつも通りの手順でプロジェクトやアプリの作成等を行なっていくことになります。

プロジェクトの作成

まずは、適当な作業フォルダに移動し、その後に下記コマンドを実行してプロジェクトを作成します。今回はプロジェクト名は paginationtest としたいと思います。

% django-admin startproject paginationtest

上記コマンドの実行によって paginationtest フォルダが作成されますので、下記コマンドで paginationtest フォルダ内に移動します。

% cd paginationtest

アプリの作成

続いて、下記コマンドを実行してアプリの作成を行います。今回はアプリ名は appli としたいと思います。

% python manage.py startapp appli

さらに、今いるフォルダの中に paginationtest フォルダが存在するはずですので、そのフォルダの下にある settings.py を開き、INSTALLED_APPS の定義を下記のように変更します。1行目に 'appli', を追加すれば良いだけです。

settings.pyの変更
INSTALLED_APPS = [
    'appli',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

これにより、アプリ appli がプロジェクト paginationtest に登録されることになります。

ビューの作成

次はビューを作成していきます。今いるフォルダの中に appli フォルダが存在し、その中に views.py が存在するはずですので、この views.py を下記のように変更してください。usersdate_joined フィールドに対して昇順に整列するようにしています(date_joined は User の持つフィールドで、インスタンスの作成日時が記録されるフィールドです)。

views.py
from django.shortcuts import render
from django.contrib.auth.models import User

def index(request):
    users = User.objects.all().order_by('date_joined')

    context = {
        'users' : users
    }

    return render(request, 'appli/users.html', context)

テンプレートの作成

次はテンプレートを作成していきます。まず、appli フォルダの中に templates フォルダ、さらに templates フォルダの中に appli フォルダを作成してください。

そして、最後に作成した appli フォルダの中に users.html を新規作成し、下記のように中身を変更してください。

appli/users.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>ユーザー一覧</title>
</head>
<body>
    <main class="container my-5 bg-light">
        <h2>ユーザー一覧</h2>
        <table class="table table-hover">
            <thead>
                <tr><th>ユーザー名</th><th>登録日</th></tr>
            </thead>
            <tbody>
                {% for user in users %}
                <tr>
                    <td>{{ user.username }}</td>
                    <td>{{ user.date_joined|date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </main>
</body>
</html>

ちょっとソースコードが長いので複雑にも思えますが、単にテーブルを作成し、ビューから渡された users に含まれる各 User のインスタンスの情報を users に対する for ループで1つ1つテーブルのセルの中に出力しているだけになります。ただ、見た目を整えるために、bootstrap を読み込んだり class の設定なども行っています。

User のインスタンスの情報としては usernamedate_joined フィールドの値を出力するようにしており、date_joinedUser のインスタンスが作成された日時となります。

URL のマッピング

次は先ほど作成したビューと URL のマッピングを行います。まず、paginationtest フォルダの下にある urls.py を下記のように変更してください。

paginationtest/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('appli/', include('appli.urls'))
]

これにより、リクエストされた URL がルートパス指定で /appli/ から始まる場合に appli フォルダ内の urls.py に従って実行するビューの関数が選択されるようになります。が、現状 appli フォルダ内に urls.py は存在しないため、appli フォルダ内に urls.py を新規作成し、その urls.py の中身を下記のように変更してください。

appli/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index')
]

マイグレーションの実行

以上で、ページネーションを “利用しない” 例としてのソースコードの変更は完了となります。

ただし、ページネーションを “利用しない” 場合と “利用する” 場合の差を確認するためには、モデルのインスタンスが複数用意されている必要があります。その準備をここから行なっていきます。

今回は、この章の最初でも説明したように、モデルクラスとしては User クラスとの使い分け で紹介した User を利用します。この Userauth アプリにあらかじめ用意されたモデルクラスであり、別途 models.py で定義する必要はありません。そして、データベースに保存された User のインスタンスの一覧が表示されるように views.pyusers.html が作成されています。なので、User のインスタンスを複数作成しておけば、その一覧がウェブアプリから確認できることになります。

ただし、この User のインスタンスの作成先は User に対応するテーブルであるため、このテーブルを事前に作成しておく必要があります。そして、このテーブルの作成はマイグレーションによって実現することができ、マイグレーションは下記のコマンドで実行することができます。

% python manage.py migrate

スーパーユーザーの作成

User のテーブルが作成できたので、次は User のインスタンスを作成していきます。

このインスタンスの作成は、どんな手段で行っても良いのですが、ここでは管理画面からインスタンスの作成を行なっていきます。以降では、このインスタンスの作成手順についても簡単に説明していきますが、管理画面の使い方に関しては下記ページで別途解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

Djangoの管理画面の使い方の解説ページアイキャッチ 【Django入門11】管理画面(admin)の使い方の基本

ということで、インスタンスの作成を行っていきましょう!まずは、管理画面にログインするためのスーパーユーザーを作成します。このスーパーユーザーは、下記のコマンド出作成可能です。

% python manage.py createsuperuser

コマンドを実行すればユーザー名とメールアドレスとパスワード(確認用も含めて2回)の入力が促されますので適当なものを入力してください。最後に Superuser created successfully. と表示されればスーパーユーザーの作成に成功したことになります。メールアドレスやパスワードは出鱈目なものを入力するとエラーになる可能性があるので注意してください。メールアドレスやパスワードとして妥当なものを入力する必要があります。

開発用ウェブサーバーの起動

次は、管理画面にログインするために下記コマンドで開発用ウェブサーバーを起動しておきたいと思います。開発用ウェブサーバーが起動することで、サーバーがリクエストを受信することが可能となり、受信したリクエストに応じてアプリが動作することになります。そして、これによって管理画面も利用可能となります。

% python manage.py runserver

ユーザーの作成

次は、管理画面でユーザーの作成を行なっていきます。

まず、ウェブブラウザから下記 URL を開いてください。

http://localhost:8000/admin/

管理画面へのログインフォームが表示されるはずですので、Username フィールドと Password フィールドに、先ほど作成したスーパーユーザーのユーザー名とパスワードをそれぞれ入力して Log in ボタンをクリックしてください。

ログインフォーム

ログインすれば下の図のような管理画面のトップページが表示されると思いますので、Users の右側にある Add リンクをクリックしてください。

管理画面のトップページ

そうすると、下の図のようなユーザー追加フォームが表示されます。このフォームでユーザーの追加を行うことができますので、適当なユーザー名とパスワードを入力して SAVE ボタンのクリックを行ってください。

ユーザーの追加フォーム

ポイントは、ユーザー名に関しては重複が許されないという点と、パスワードは重複しても良いという点になります。なので、ユーザー名は個別に考える必要がありますが、パスワードに関しては毎回同じもので良いです。ですが、フォームに記載されている通り、ユーザー名と似ているパスワードや数字だけのパスワード等は許可されていないので注意してください。

SAVE ボタンをクリックしてユーザーの追加に成功すれば、下の図のような画面が表示されます。これで、User のインスタンスが1つ追加できたことになります。また、下の図の画面ではユーザーの詳細情報の変更を行うこともできるのですが、今回はこの変更は不要なので、画面左側にある Users の右の Add リンクをクリックしてください。

ユーザー追加後に遷移するページ

Add リンクをクリックすれば再びユーザー追加フォームが表示されることになります。あとは、これらを繰り返して User のインスタンスを複数作成しておいてください。最低でも、合計で5つ程度の User のインスタンスを作成しておいてください。ちなみに、最初にコマンドで作成したスーパーユーザーも User のインスタンスの1つとなるため、スーパーユーザーを含めて5人分の User のインスタンスを作成しておけばオーケーです。

動作確認

ちょっと準備に時間がかかりましたが、これで一旦ページネーションを “利用しない” 場合の動作確認が可能となったことになります。ということで、ウェブブラウザから下記 URL を開いてください。

http://localhost:8000/appli/

これにより、ウェブラウザから開発用ウェブサーバーにリクエストが送信され、上記 URL にマッピングされた views.pyindex 関数が実行されます。そして、その index 関数の中で Users のインスタンスが全て取得され、それがテンプレートファイル users.html に埋め込まれてページとして表示されることになります。

その結果は、下の図のようなものになると思います。

ページネーションを利用しない場合に表示されるページ

今回の例ではページネーションを利用していないため、全インスタンスが1つのページに表示されることになります。これは、インスタンスの数が増えたとしても同様になります。

次は、このアプリを変更し、ページネーションを利用してインスタンスが複数のページに分割されて表示されるようにしていきたいと思います。

一旦、ここで動作確認は終了となるため、開発用ウェブサーバーは終了しておいてください。開発用ウェブサーバーを起動したターミナルやコマンドプロンプト等で ctrl + c を入力すれば開発用ウェブサーバーを強制終了できるはずです。

また、ページネーションからは話が逸れるのですが、ここまでの手順で実施したように、管理画面を利用することで、自身でフォームを用意しなくてもモデルクラスのインスタンスの追加(今回は User のインスタンスの追加)を行うことが可能となります。これにより、ちょっとしたウェブアプリの動作確認を短手番で行うこともできるようになりますので、管理画面は使いこなせるようになっておいた方が良いと思います。管理画面については、Django 入門 の連載における下記ページで解説していますので、使い方を忘れてしまった方は是非この機会に下記ページで思い出しておきましょう!

Djangoの管理画面の使い方の解説ページアイキャッチ 【Django入門11】管理画面(admin)の使い方の基本 【Django入門12】管理画面のカスタマイズ(ModelAdmin)

スポンサーリンク

ページネーションを利用するアプリ(ページの分割)

少し話が逸れてしまいましたが、ここからは先ほど作成したページネーションを “利用しない” アプリを、ページネーションを “利用する” アプリに変更していきたいと思います。

ここでの変更内容は、大きく分けると、まず ページネーションを利用する場合の処理の流れ で説明した流れを実現できるようにビューを変更することと、ページの情報を取得する で紹介した方法を利用してページに様々な情報やリンクを追加できるようテンプレートファイルを変更することの2つになります。このために、先ほど作成した views.pyusers.html の変更を行っていきます。

views.py の変更

まずは views.py の変更を行なっていきます。結論としては、ページネーションによりページの分割を行うためには下記のように views.py を変更してやれば良いです。

変更後のviews.py
from django.shortcuts import render
from django.contrib.auth.models import User
from django.core.paginator import Paginator

def index(request):
    users = User.objects.all().order_by('date_joined')

    # ページの分割
    paginator = Paginator(users, 2)

    # クエリパラメーターからページ番号取得
    number = int(request.GET.get('p', 1))

    # 取得したページ番号のページを取得
    page_obj = paginator.page(number)

    # ページをコンテキストとして渡す
    context = {
        'page_obj' : page_obj
    }

    return render(request, 'appli/users.html', context)

上記の index 関数は、ページネーションを利用する場合の処理の流れ で説明した流れの処理が実行されるようになっており、ページネーションを利用しない場合に比べて、ページの分割、ページ番号の取得、ページの取得の処理を追加しています。また、テンプレートに渡すコンテキストで、キー名を page_obj に変更しているので、その点にも注意してください。

まず、ページの分割に関しては、ページを分割する で解説した通り、Paginator() の実行、すなわち Paginator のコンストラクタの実行によって実施しています。第1引数にはデータベースから取得したクエリーセット users を指定しており、今回の場合は、User のテーブル内の全レコード(インスタンス)がこのクエリーセット users に含まれていることになります。さらに、第2引数には 2 を指定しているため、users の各インスタンスが 2 つずつ各ページに割り付けられるようにページの分割が行われることになります。

インスタンスの集合の整列 で説明したように、Paginator のコンストラクタの第1引数に指定するインスタンスの集合(クエリーセット)は事前に整列しておく必要があるという点に注意してください。上記では、index 関数の最初の行で order_by('date_joined') メソッドを実行して整列を実施しています。

次に行っているのが、ページ(Page のインスタンス)の取得になります。ページを取得する でも解説したように、ページの取得は、”ページ番号の取得” と “そのページ番号のページの取得” の2段階の処理によって実現することになります。ページ番号に関しては、request.GET.get('p', 1) によって「変数名が p のクエリパラメーター」から取得するようにしています。変数名が p のクエリパラメーターが指定されていない場合は、取得されるページ番号はデフォルトの 1 となります。そして、クエリパラメーターから取得したページ番号を引数に指定して Paginatorpage メソッドを実行し、指定したページ番号に対応する Page のインスタンス page_obj の取得を行なっています。

そして、この page_obj をコンテキストに設定してテンプレートに渡すようにしています。テンプレートには変数名 'page_obj' として page_obj が渡されることになります。

users.html の変更

続いてテンプレートファイルの users.html を変更していきます。

変更後の users.html は下記のようになります。元々の users.html から、{% for user in users %} の部分の {% for user in page_obj %} への置き換えのみを行っています。

変更後のusers.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>ユーザー一覧</title>
</head>
<body>
    <main class="container my-5 bg-light">
        <h2>ユーザー一覧</h2>
        <table class="table table-hover">
            <thead>
                <tr><th>ユーザー名</th><th>登録日</th></tr>
            </thead>
            <tbody>
                {% for user in page_obj %}
                <tr>
                    <td>{{ user.username }}</td>
                    <td>{{ user.date_joined|date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </main>
</body>
</html>

変更前のビューからは、テンプレートファイルに 'users' という変数名で User のインスタンスの集合が渡されていました。そして、テンプレートファイルでは、その users に対して for ループを行うことで User のインスタンスを1つ1つ取得して情報を表示していました。

forループによりusersのインスタンスが1つ1つ表示される様子

変更後のビューでは、'page_obj' という変数名で Page のインスタンスが渡されるように変更されています。ただ、ページの情報を取得する で解説した通り、この Page はリストや集合と同様にイテラブルなオブジェクトであり、Page に対して for ループを行うことで、Page に割り付けられたモデルクラスのインスタンス(今回の場合は User のインスタンス)を1つ1つ取得して情報を表示するようなことが可能です。

なので、もともとのテンプレートファイルでは users に対してループを実行していたところを、page_obj に対してループを実行するように変更するだけで、page_obj に割り付けられた User のインスタンスの一覧表を表示することができることになります。

forループによりpage_objに割り付けられたインスタンスが1つ1つ表示される様子

そして、users にはデータベースから取得した User のインスタンス全てが含まれていますが、Page には、そのページに割り付けられた User のインスタンスしか含まれていないため、上記のように変更するだけで、分割後のページの表示が実現できることになります。

で、これに関してはすでにお気づきの方も多いと思いますが、今回のテンプレートファイルの変更は、ビューから渡されるコンテキストのキー名が 'users' から 'page_obj' に変わったために必要となった変更となります。

コンテキストのキー名の変更

このキー名を同じものにしておけば、実はテンプレートファイルは変更不要だったことになります。単純に、ページの分割のみを行うだけであれば、コンテキストのキー名を変更しなければテンプレートファイルはページネーションの利用の有無に関わらず、同じものを利用することができます。

動作確認

一旦ここで動作確認を行なっておきましょう!

先ほどと同様の手順で開発用ウェブサーバーを起動し、ウェブアプリから下記 URL を開いてください。

http://localhost:8000/appli/

すると、下の図のようなページが表示されると思います。先程は全ての User のインスタンスが表示されていたのに対し、今回は 2 つ分のインスタンスのみが表示されていることが確認できます。これは、Paginator() 実行時に第2引数(per_page 引数)に 2 を指定しているためになります。

1ページ目の表示結果

さて、先程ウェブブラウザに指定した URL にはクエリパラメータが指定されていません。そのため、views.pyindex 関数ではページ番号としてデフォルトの 1 が表示されるようになっています。

次は、クエリパラメーターを追加して再度ページの表示を行なってみましょう!ということで、次は下記の URL をウェブブラウザに指定して開いてください。

http://localhost:8000/appli/?p=2

すると、表示されるインタンスの情報が変化することを確認できると思います。これは、クエリパラーメーターの指定によって、2 ページ目に割り付けられたインスタンスが表示されるようになったためです。

2ページ目の表示結果

User のインスタンスを5つ以上作成している場合、クエリパラメーター部分を ?p=3 に変更した場合も、また今回とは異なるインスタンスが表示されることを確認することができます。

ここまで確認できれば、ページネーションを利用する場合の処理の流れ で説明した流れの処理が実現できており、ページを分割する で説明したページの分割、さらには ページを取得する で説明したクエリパラメーターからのページ番号の取得、さらには、そのページ番号の Page の取得も実現できていると考えて良いです。

ページネーションを利用するアプリ(リンクの設置)

とりあえず、これでページの分割が実現できたことになるのですが、ユーザーが毎回 URL のクエリパラメーターを直接変更する必要があり、ユーザーにはちょっと不親切です。次のページや前のページへのリンクを表示し、リンクのクリックでユーザーが表示したいページに遷移するようにしてあげたほうが便利ですね!また、表示中のページのページ番号やページの総数などの情報も表示してあげたほうが親切です。

ということで、次はページの情報の表示や前後のページへのリンク等の設置の例を示していきたいと思います。ビューからは既に Page のインスタンスがテンプレートファイルに渡されるようになっており、 ページの情報を取得する で解説したように、Page のインスタンスから上記のような情報は取得可能ですので、ここで変更するのは users.html のみとなります。

今回は、下記のように users.html を変更したいと思います。

ページの情報の表示
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>ユーザー一覧</title>
</head>
<body>
    <main class="container my-5 bg-light">
        <h2>ユーザー一覧</h2>
        <table class="table table-hover">
            <thead>
                <tr><th>ユーザー名</th><th>登録日</th></tr>
            </thead>
            <tbody>
                {% for user in page_obj %}
                <tr>
                    <td>{{ user.username }}</td>
                    <td>{{ user.date_joined|date }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        <div class="pagination">
            <span class="step-links">
                {% if page_obj.has_previous %}
                    <a href="?p=1">« first</a>
                    <a href="?p={{ page_obj.previous_page_number }}">previous</a>
                {% endif %}
                <span class="current">
                    Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
                </span>
                {% if page_obj.has_next %}
                    <a href="?p={{ page_obj.next_page_number }}">next</a>
                    <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
                {% endif %}
            </span>
        </div>
    </main>
</body>
</html>

前述の users.html から太字部分、つまり <div class="pagination"><div> のコードを追加し、下の図のように表示中のページの情報や前後のページへのリンクの表示が行われるように変更しています。

追加したコードによって出力されるリンク・情報

で、このようなリンクの出力を行うために、page_obj (Page のインスタンス) のデータ属性やメソッドを利用しています。追加したコードの中で利用している各種データ属性やメソッドの意味合いに関しては、ページの情報を取得する を参照していただければと思います。

ポイントは、a タグの href?p=ページ番号 を指定する必要があるという点になります。これは、ページを取得する で説明したように、ビューでクエリパラメーターから ページ番号 を取得できるようにするために必要となります。

上記 users.htmlでは、他のページへのリンクとして「最初のページ」「前のページ」「次のページ」「最後のページ」へのリンクを設置しており、これらの ページ番号 はそれぞれ下記によって取得しています(page_obj はコンテキストとして渡される Page のインスタンス)。これらはページネーションを利用する場合に利用する機会の多いデータ属性やメソッドになりますので、この機会に覚えておくとよいと思います。

  • 最初のページ:1
  • 前のページ:page_obj.previous_page_number
  • 次のページ:page_obj.next_page_number
  • 最後のページ:page_obj.paginator.num_pages

動作確認

これでコードの変更は完了したので動作確認を行なっていきます。

まず開発用ウェブサーバーを起動させ、続いて下記の URL をウェブブラウザで表示してください。

http://localhost:8000/appli/

すると、下の図のようなページが表示され、インスタンスの一覧の下側に次のページへのリンク(next)と最後のページへのリンク(last)が表示されていることが確認できると思います。また、全体のページ数と今表示しているページのページ番号(1 of 3)も表示されていることが確認できると思います。

インスタンス一覧の下にリンクが表示されている様子1

前のページへのリンクが表示されていないのは、今表示しているのが 1 ページ目であり、前のページが存在しないからになります。

続いて next リンクをクリックしてください。これにより、表示されるインスタンスが変化することが確認できるはずです。また、User のインスタンスが5つ以上作成されている場合、2 ページ目を表示すれば前のページへのリンク(previous)と次のページへのリンク(next)の両方が表示されます。

インスタンス一覧の下にリンクが表示されている様子2

さらに、ウェブブラウザの URL バーを確認すると、下記のようにクエリパラメーター ?p=2 が表示されているはずです。このように、リンクがクリックされた際にクエリパラメーターを設定し、ビュー側でクエリパラメーターからページ番号を取得するようにすることで、分割後のページ間での遷移が実現できます。ページの分割だけでなく、ページ番号を取得するという点も重要になるので、その取得の仕方も是非覚えておいてください!

http://localhost:8000/appli/?p=2

掲示板アプリでページネーションを利用してみる

最後に、いつも通りの流れで、この Django 入門 の連載の中で開発してきている掲示板アプリに対し、ページネーションを適用していきたいと思います。

この Django 入門 に関しては連載形式となっており、ここでは以前に下記ページの 掲示板アプリの管理画面をカスタマイズしてみる で作成したウェブアプリに対してページネーションを導入していきたいと思います。

【Django入門12】管理画面のカスタマイズ(ModelAdmin)

現状、このアプリにはモデルクラスのインスタンスの一覧を示すページとして「ユーザー一覧」「コメント一覧」があります。また、各ユーザーのコメント履歴が「ユーザーの詳細」ページで表示されるようになっています。

各インスタンスの一覧が表示されるページ

これらは1ページで全てのインスタンスが表示されるようになっているため、このページで紹介したページネーションを導入して複数のページに分割されるようにしていきたいと思います。

スポンサーリンク

掲示板アプリのプロジェクト一式の公開先

この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。

https://github.com/da-eu/django-introduction

また、前述のとおり、ここでは前回の連載の 掲示板アプリの管理画面をカスタマイズしてみる で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-modeladmin

さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。以降では、基本的には前回からの差分のみのコードを紹介していくことになるため、変更後のソースコードの全体を見たいという方は、下記からプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-pagination

コメント一覧の変更

前述のとおり、掲示板アプリでは既にインスタンスの一覧の表示は行えるようになっていますので、この一覧表示をページネーションでページ分割していきます。まずは、コメントの一覧表示にページネーションを導入していきます。

ここまでの解説からも分かるように、変更が必要になるのはビューとテンプレートのみとなります。

ビューの変更

まず、コメント一覧の表示を実現しているのは view.py における下記の comments_view 関数になります。

まずは、この comments_view を例に、ページネーションを利用するように変更していきたいと思います。

変更前のcomments_view
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required

# 略

@login_required
def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)

といっても、変更内容は ページネーションを利用するアプリ(ページの分割) で示した通りで、インスタンスの集合を引数に指定して Paginator() を実行することでページの分割を行い、クエリパラメーターからページ番号を取得、さらにそのページ番号の Page のインスタンスを page メソッドで取得し、その Page のインスタンスをコンテキストにセットしてやれば良いだけです。

ということで、変更後の comments_view は下記のようなものになります。django.core.paginator からの Paginatorimport を忘れないようにご注意ください。

変更後のcomments_view
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator

# 略

@login_required
def comments_view(request):
    comments = Comment.objects.all().order_by('date')

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'forum/comments.html', context)

まず、インスタンスの集合の整列 で説明したように、Paginator のコンストラクタの第1引数に指定するインスタンスの集合は整列しておく必要があるため、comments_view の1行目の最後で order_by('date') メソッドが実行されるように変更しています。そして、その整列後のインスタンスの集合 comments を引数に指定して Paginator のコンストラクタを実行し、ページ分割を行うようにしています(各ページに表示されるインスタンスの数は 3 としています)。

また、ページ番号の取得を行う request.GET.get('p', 1) の第2引数には重要な意味合いがあるため、この引数の指定を忘れないように注意してください。この第2引数は、URL にクエリパラメーター p が存在しなかった場合のデフォルト値として利用されます。第2引数が指定されなかった場合、URL にクエリパラメーター p が存在しないと None が返却されることになります。そして、Nonepage メソッドに指定して実行すると例外が発生します(そもそも上記の場合は int() 実行時に例外が発生します)。

掲示板アプリでは、各ページに下の図のようなナビゲーションバーを表示しており、リンクのクリックでコメント一覧ページを表示できるようになっています。ですが、このリンクのクリック時にはクエリパラメーターは指定されません。そのため、第2引数を指定せずに request.GET.get('p') を実行するようにしてしまうと、このナビゲーションバーのリンクがクリックされた時に毎回例外が発生することになってしまいますので注意してください。

掲示板アプリのナビゲーションバー

ちなみに、今回は request.GET.get の第2引数の指定によって、クエリパラメーターが存在しない場合のケアを行いますが、ナビゲーションバーのリンクを変更して、必ずクエリパラメーターが付加されるように変更しても問題ありません。

テンプレートの変更

続いて、テンプレートファイル側の変更を行っていきます。

コメント一覧ページの基になっているテンプレートファイルは comments.html であり、変更前の comments.html は下記になります。

変更前のcomments.html
{% extends "forum/base.html" %}

{% block title %}
コメント一覧
{% endblock %}

{% block main %}
<h1>コメント一覧(全{{ comments|length }}件)</h1>
<table class="table table-hover">
    <thead>
        <tr>
            <th>本文</th><th>投稿者</th>
        </tr>
    </thead>
    <tbody>
        {% for comment in comments %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
            {% if comment.user is not None %}
            <td>{{ comment.user.username }}</td>
            {% else %}
            <td>不明</td>
            {% endif %}
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

テンプレートファイルの変更に関しても、ページネーションを利用するアプリ(ページの分割) と ページネーションを利用するアプリ(リンクの設置) で説明した内容に基づいて変更を行えば良いです。特に、前後のページへのリンクの設置に関しては ページネーションを利用するアプリ(リンクの設置) で紹介したものをそのままコピペして利用できます。

ただし、ビューからは comments という名前で Comment のインスタンスの集合が渡されるのではなく、page_obj という名前で Page のインスタンスが渡されるようになっていますので、この変化には注意が必要です。

特に、コメント一覧(全{{ comments|length }}件) の部分の comments|length は「コメントの全件数」を表示するための記述となっています。この部分を page_obj|length にそのまま変更してしまうと「その page_obj に割り付けられているインスタンス(コメント)の数」が表示されてしまうことになり、コメントの全件数が表示されなくなってしまいます。全てのインスタンスの数は page_obj.paginator.count によって取得できるため、comments|lengthpage_obj.paginator.count に書き換える必要があります。

これらを考慮すると、comments.html を下記のように変更すればページネーションが実現できることになります。

変更後のcomments.html
{% extends "forum/base.html" %}

{% block title %}
コメント一覧
{% endblock %}

{% block main %}
<h1>コメント一覧(全{{ page_obj.paginator.count }}件)</h1>
<table class="table table-hover">
    <thead>
        <tr>
            <th>本文</th><th>投稿者</th>
        </tr>
    </thead>
    <tbody>
        {% for comment in page_obj %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
            <td>{{ comment.user.username }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

ユーザー一覧の変更

続いてユーザー一覧の変更を行っていきますが、変更内容はコメント一覧の時と同様となりますので、ここでは変更後のビューの関数 users_view と テンプレートファイル users.html のみを示しておきます。

まず、変更後の users_view の関数は下記のようになります。

変更後のusers_view
@login_required
def users_view(request):
    users = User.objects.all().order_by('date_joined')

    paginator = Paginator(users, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'forum/users.html', context)

続いて、変更後の users.html は下記のようになります。

変更後のusers.html
{% extends "forum/base.html" %}

{% block title %}
ユーザー一覧
{% endblock %}

{% block main %}
<h1>ユーザー一覧(全{{ page_obj.paginator.count }}人)</h1>
<table class="table table-hover">
   
    <thead>
        <tr>
            <th>ユーザー</th><th>コメント数</th>
        </tr>
    </thead>
    <tbody>
        {% for user in page_obj %}
        <tr>
            <td><a href="{% url 'user' user.id %}">{{ user.username }}</a></td>
            <td>{{ user.comments.all|length }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

スポンサーリンク

ユーザー詳細の変更

最後に、ユーザー詳細ページの変更を行っていきます。

このユーザー詳細ページでは、ユーザーの情報の表示とユーザーのコメント履歴を表示するようにしています。このコメント履歴部分のみをページ分割していきたいと思います。

ページ分割を行う部分を示す図

ビューの変更

ユーザーの詳細を表示するビューの関数は user_view であり、変更前の user_view は下記のようになっています。

変更前のuser_view.py
@login_required
def user_view(request, user_id):
    user = get_object_or_404(User, id=user_id)

    context = {
        'user' : user
    }

    return render(request, 'forum/user.html', context)

また、ユーザーの詳細ページを表示する際に利用するテンプレートファイル user.html は下記のようなファイルになっています。

変更前のuser.html
{% extends "forum/base.html" %}

{% block title %}
{{ user.username }}
{% endblock %}

{% block main %}
<h1>ユーザー({{ user.id }})</h1>
<h2>{{ user.username }}の情報</h2>
<table class="table table-hover">
    <tbody>
        <tr><th>名前</th><td>{{ user.username }}</td></tr>
        <tr><th>連絡先</th><td>{{ user.email|urlize }}</td></tr>
        <tr><th>年齢</th><td>{{ user.age }}</td></tr>

    </tbody>
</table>
<h2>{{ user.username }}のコメント履歴</h2>
<table class="table table-hover">
    <tbody>
        {% for comment in user.comments.all %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

ページネーションの利用手順に関しては、基本的には comments_viewusers_view の時と同じなのですが、上記の user_view の通り、現在データベースから取得しているのもテンプレートファイルに渡しているのも User のインスタンス1つ(user)のみとなっています。そして、テンプレートファイルから {% for comment in user.comments.all %} によって、user に関連付けられた Comment の全インスタンスが取得され、各 Comment の本文が出力されることで、ユーザーが投稿したコメント一覧が表示されるようになっています。

ユーザー詳細ページのコメント一覧が表示される仕組み

つまり、User のインスタンスさえ渡してやれば、User のインスタンスに関連付けられた Comment のインスタンスの集合はテンプレートファイル側で取得できるため、ビューでの Comment のインスタンスの集合の取得は不要でした。

ですが、ページ分割はテンプレートファイルでは実施できないため、今回のように Comment のインスタンスの集合に対するページ分割を実施するのであれば、ビューで Comment のインスタンスの集合を取得する必要があります。具体的には、user_view で、user に関連付けられた Comment のインスタンスの集合の取得が必要となります。これさえ取得できれば、後はいつも通りの手順で Comment のインスタンスの集合に対してページ分割等を行っていけばよいことになります。

で、その “user に関連付けられた Comment のインスタンスの集合の取得” を行うようにし、さらに Comment のインスタンスの集合に対してページ分割を行うように変更した user_view 関数は下記のようになります。

変更後のuser_view
@login_required
def user_view(request, user_id):
    user = get_object_or_404(User, id=user_id)
    comments = Comment.objects.filter(user=user).order_by('date')

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'user' : user,
        'page_obj' : page_obj
    }

    return render(request, 'forum/user.html', context)

Comment.objects.filter(user=user)user に関連付けられた Comment のインスタンスの集合の取得を行い、さらに order_by('date') によって、そのインスタンスの集合を date フィールドに対して昇順に整列させるようにしています。その後は、前述のとおり、いつも通りの手順でページネーションを実施しています。ただし、テンプレートファイルに渡すコンテキストには Page のインスタンスである page_obj だけでなく、User のインスタンスである user も必要となるので注意してください。ユーザーの詳細ページの上側の項目は User のインスタンスによって出力しているため、変更前と同様に、テンプレートファイルには  User のインスタンス user も渡す必要があります。

テンプレートファイルに Page のインスタンスを渡すことができれば、あとはテンプレートファイル側で Page のインスタンスから必要な情報を取得して出力してやれば良いだけになります。

MEMO

上記では “User” と記していますが、正確に言えば User ではなく CustomUser (get_user_model の返却値) が正しいです

ですが、説明を簡単にするため、ここでは User と記して説明を行っています

テンプレートの変更

ということで、テンプレート側では、ユーザーの情報を表示する部分は User のインスタンスを利用し、コメントの情報を表示するのに Page のインスタンスを利用すれば良いことになります。

再掲になりますが、ユーザーの詳細ページのテンプレートファイル user.html は現状では下記のようになっています。

変更前のuser.html
{% extends "forum/base.html" %}

{% block title %}
{{ user.username }}
{% endblock %}

{% block main %}
<h1>ユーザー({{ user.id }})</h1>
<h2>{{ user.username }}の情報</h2>
<table class="table table-hover">
    <tbody>
        <tr><th>名前</th><td>{{ user.username }}</td></tr>
        <tr><th>連絡先</th><td>{{ user.email|urlize }}</td></tr>
        <tr><th>年齢</th><td>{{ user.age }}</td></tr>

    </tbody>
</table>
<h2>{{ user.username }}のコメント履歴</h2>
<table class="table table-hover">
    <tbody>
        {% for comment in user.comments.all %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>
{% endblock %}

{% for comment in user.comments.all %} は、user に関連付けられた Comment のインスタンスの全てに対する for ループとなっているため、この user.comments.allpage_obj に置き換えれば、その page_obj に割り当てられたインスタンスのみに対する for ループが実現できることになります。

あとは、コメントの一覧表の下側に、他のページへのリンク等を追加してやれば良いだけです。ここに関しては、前述で変更した comments.html と同様に ページネーションを利用するアプリ(リンクの設置) で示したものをそのままコピペすればオーケーです。

ということで、ユーザーの詳細ページのコメント一覧にページネーションを導入するための変更を行った user.html は下記のようになります。

変更後のuser.html
{% extends "forum/base.html" %}

{% block title %}
{{ user.username }}
{% endblock %}

{% block main %}
<h1>ユーザー({{ user.id }})</h1>
<h2>{{ user.username }}の情報</h2>
<table class="table table-hover">
    <tbody>
        <tr><th>名前</th><td>{{ user.username }}</td></tr>
        <tr><th>連絡先</th><td>{{ user.email|urlize }}</td></tr>
        <tr><th>年齢</th><td>{{ user.age }}</td></tr>

    </tbody>
</table>
<h2>{{ user.username }}のコメント履歴</h2>
<table class="table table-hover">
    <tbody>
        {% for comment in page_obj %}
        <tr>
            <td><a href="{% url 'comment' comment.id %}">{{ comment.text|truncatechars:20 }}</a></td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

動作確認

以上で、ページネーションを利用するための実装は完了したことになります。

最後に動作確認を行なっていきましょう!

開発用ウェブサーバーの起動

まずは前準備として、いつも通り開発用ウェブサーバーの起動を行います。manage.py が存在するフォルダ(testproject フォルダ)に移動し、下記コマンドを実行して開発用ウェブサーバーを起動してください。

python manage.py runserver

今回は models.py の変更は行なっていないため、マイグレーション等の実施は不要となります。

コメント一覧ページの表示の確認

続いて、ウェブブラウザを起動し、下記 URL を開きます。

http://localhost:8000/forum/

上記 URL を開くとコメント一覧ページもしくはログインフォームが表示されることになると思います。

ログインフォームが表示された方はログアウトされている状態であるため、ログインフォームからまずはログインを行なってください。ログインすれば、ログインしたユーザーの詳細情報のページが表示されることになりますが、このページについては後から確認しますので、まずはナビゲーションバーの コメント一覧 をクリックしてコメント一覧ページを表示してください。

また、ログイン可能なユーザー名・パスワードを忘れてしまった方は、ユーザー登録 リンクをクリックしてユーザー登録を行なっていただければログインすることが可能です。この場合も、登録したユーザーの詳細情報のページが表示されることになりますが、ナビゲーションバーの コメント一覧 をクリックしてください。

ということで、これで皆さんがコメント一覧ページを見ている状態になっていると思いますので、まずはコメント一覧ページの表示内容を確認していきましょう!

コメント一覧ページでは、今まで投稿済みのコメントが全てページ内に表示されるようになっていたのですが、今回の変更によって表示されるコメントの数は各ページで 3 以下となるようになっています。そして、投稿済みのコメントの数が 4 以上の場合、ページの分割が行われ、コメント一覧の下の方に次のページへのリンク(next)や最後のページへのリンク(last)が表示されるようになっています。

表示されるコメント一覧ページ

コメントの数が 4 未満の場合は次のページへのリンク(next)や最後のページへのリンク(last)が表示されないため、ナビゲーションバーの コメント投稿 をクリックし、コメントを投稿してコメント数を増やしてみてください。コメントの数が 4 以上になれば、コメント一覧の下にリンクが表示されるようになるはずです。

そして、これらのリンクをクリックすれば、次のページや最後のページを表示できることが確認できると思います。また、例えば次のページのリンクをクリックすれば、今度は前のページへのリンク(previous)や最初のページへのリンク(first)も表示されることが確認でき、これらをクリックすれば、前のページや最初のページに遷移することも確認できると思います。

次へをクリックした時に表示されるページ

このように、Paginator を利用することでページの分割を実現することができ、さらにリンクを設置することで分割後の各ページへの遷移をマウス操作で実現することができるようになります。

ユーザー一覧ページの表示の確認

また、上記ではコメント一覧を例に説明を行いましたが、ユーザー一覧やユーザーの詳細ページに表示されるコメント履歴に関しても同様のことが確認できると思います。

ナビゲーションバーの ユーザー一覧 をクリックすればユーザー一覧が表示され、ここでもページ分割が行われていることが確認できると思います。この場合もユーザー数が 4 未満の場合は1ページで全てのユーザーが表示されることになりますが、ナビゲーションバーの ユーザー登録 からユーザーの登録を行えばユーザー数を増やすことができ、ユーザー数が 4 以上になればページの分割が行われることが確認できると思います。

ユーザー一覧がページ分割されている様子

ユーザーの詳細ページの表示の確認

また、このユーザー一覧から特定のユーザーの名前をクリックすることで、そのユーザーの詳細ページが表示され、そこでコメント履歴も確認することができます。このコメント履歴に関しても同様のことが確認できると思います。

コメント履歴がページ分割されている様子

以上で動作確認は完了です!

ページが分割されることで、各ページの見た目もスッキリしたことが確認できたのではないかと思います。今回は開発段階のアプリにページネーションを導入したのでメリットを感じにくかったかもしれませんが、アプリの利用者が増えるとページに表示するインスタンスの数も増えることになるため、アプリが人気になればなるほどページネーションのメリットが大きくなります。

ただ、ページを分割するだけだと表示される情報が減るだけなので、他のページへのリンクの設置を設置して、前後のページ等を簡単に閲覧できるようにすることも重要となります。この点についても是非覚えておいてください!

まとめ

このページでは、Django のページネーションについて解説をしました!

Django では Paginator を利用することで簡単にページネーションを実現することができます。特に、ページ分割に関しては簡単に実現できることが、このページの解説からも理解していただけたのではないかと思います。また、分割後のページへの遷移をマウス操作で実現するためにはリンクの設置も必要で、これに関してはややこしいようにも思えますが、基本的に一度作ってしまえば、あとは基本的にはコピペだけで実装できると思います。

アプリの開発入門者にとってはあまり魅力的な機能に思えないかもしれないですが、ウェブアプリの利用者が増えると大量のレコードがデータベースに保存されることになり、それを一度に表示すると閲覧性の悪いページになってしまいます。そういったことを改善するのにページネーションは有効ですので、是非この機能についても覚えておいてください!

次の連載では、Django でのウェブアプリ開発時に陥りやすい N + 1 問題と、その解決方法について解説します。ちょっと地味な話題になりますが、ウェブアプリ開発者には是非知っておいてもらいたい内容の解説になりますので、是非次の連載も読んでみてください!

DjangoにおけるN+1問題の解説ページアイキャッチ 【Django入門14】N+1問題とselect_related・prefetch_relatedでの解決

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です