ページネーションの実装

一覧ページ・検索結果ページにページネーションを導入しましょう。

ページネーションとは「次のページ」「前のページ」「先頭のページ」「最後のページ」、「ページナビゲーション 1, 2, 3 ...」などの機能のことです。

結果が、30件~50件を超えてくると、ページネーションを導入したくなりますね。

ページネーションのためのモジュールのインストール

Perlには、ページネーションを支援してくれるモジュールがあります。

ページネーションの基本モジュールとして「Data::Page」をインストールします。

Data::Page::Navigationは、Data::Pageにページナビゲーション機能を追加してくれるモジュールです。こちらもインストールしましょう。

モジュールのインストールはcpanmを使って行います。

cpanm Data::Page
cpanm Data::Page::Navigation

ページネーションの基礎

テーブルに本のデータが1000件存在すると仮定しましょう。

id => 1,    name => 'Book1',    price => 1200
id => 2,    name => 'Book2',    price => 2000
...
id => 1000, name => 'Book1000', price => 450

Data::Pageモジュールは、ページネーションに必要な情報を自動的に計算してくれますが、そのために必要な情報を知っておく必要があります。

全体の件数

まず最初に必要な情報は、全体の件数です。DBIx::Customのselectメソッドと、mysqlのcount関数を使って、件数を取得してみましょう。

# 全体の件数
my $total = $dbi->model('book')->select('count(*)')->value;

一ページに表示する個数

次に必要なのは、一ページに表示する個数です。これは、30~50くらいの値を設定します。

my $count_per_page = 30;

現在のページ

最後は、現在のページです。これは、パラメーターから受け取り、長さがない場合、または、1より小さい場合は、1を指定しましょう。

my $page = param('page');
$page = 1 if !length $page || $page < 1;

オフセットの計算

ひとつのページに表示するデータを取り出すには、、mysqlのlimit句を使って、どの位置からデータを取得するかを指定する必要があります。

このために、オフセットを計算します。

my $offset = $count_per_page * ($page - 1);

データの取得

mysqlのbookテーブルから、指定したページに表示するデータを取得します。DBIx::Customのselectメソッドと、mysqlのlimit句を使って、件数を取得してみましょう。appendはorder by句や、limit句など自由に追加するためのオプションです。

my $books = $dbi->model('book')->select(['id', 'name'], append => "limit $offset, $count_per_page")->all;

Data::Pageオブジェクトの生成

これで、Data::Pageオブジェクトを生成する準備が整いました。

# Data::Pageオブジェクトの生成
my $pager = Data::Page->new($total, $count_per_page, $page);

前のページを取得

前のページを取得するには、previous_pageメソッドを使用します。前のページがない場合は、undefが返ってきます。

my $previous_page = $pager->previous_page;

次のページを取得

次のページを取得するには、next_pageメソッドを使用します。次のページがない場合は、undefが返ってきます。

my $next_page = $pager->next_page;

最初のページ番号

最初のページ番号を取得するにはfirst_pageメソッドを使用します。常に1を返します。

my $first_page = $pager->first_page;

最後のページ番号

最後のページ番号を取得するにはlast_pageメソッドを使用します。これは総ページ数と同じ値になります。

my $last_page = $pager->last_page;

ページナビゲーション機能

1, 2, 3, 4などのページナビゲーション機能を追加してみましょう。たとえば15ページ目にいるときは、15ページ目の周辺のページを合わせて、ページ番号を十個取得できます。

# ページナビゲーションの個数を設定
$pager->pages_per_navigation(10);

# 10 11 12 13 14 15 16 17 18 19
my @nav_pages = $pager->pages_in_navigation;

ページネーションのサンプルコード

ページネーションのサンプルコードです。最初のページ、前のページ、次のページ、最後のページのサンプルです。ページが複数あるときだけ、ページネーションを表示するようにしています。

アプリケーションクラス

use Data::Page;
use Data::Page::Navigation;

テンプレート

<%
  # 現在のページ
  my $page = param('page');
  $page = 1 if !length $page || $page < 1;
  
  # DBI
  my $dbi = $self->app->dbi;
  
  # 1ページに表示する個数
  my $count_per_page = 30;
  
  # オフセット
  my $offset = $count_per_page * ($page - 1);
  
  # 本の一覧
  my $books = $dbi->model('book')->select(
    ['id', 'name'],
    append => "limit $offset, $count_per_page"
  )->all;
  
  # 本の総数
  my $total = $dbi->model('book')->select('count(*)')->value;
  
  # ページネーションオブジェクト
  my $pager = Data::Page->new($total, $count_per_page, $page);
%>

  % for my $book (@$books) {
    <div>
      <div>本のID <%= $book->{id} %></div>
      <div>本のタイトル <%= $book->{name} %></div>
    </div>
  % }
  
  <%
    # ページナビゲーションの個数を設定
    $pager->pages_per_navigation(10);
    
    # 前のページ
    my $previous_page = $pager->previous_page;
    
    # 次のページ
    my $next_page = $pager->next_page;
    
    # 最初のページ
    my $first_page = $pager->first_page;
    
    # 最後のページ
    my $last_page = $pager->last_page;

    # 周辺のページ番号を取得
    my @nav_pages = $pager->pages_in_navigation;
  %>
  % if ($first_page != $last_page) {
    <div class="pagenation">
      <a href="<%= url_with->query({page => undef}) %>">最初へ</a>
      % if ($previous_page) {
        <a href="<%= url_with->query({page => $previous_page}) %>">前へ</a>
      % }
      % for my $nav_page (@nav_pages) {
        % if ($nav_page eq $page) {
          <span><%= $nav_page %></a>
        % } else {
          <a href="<%= url_with->query({page => $nav_page}) %>"><%= $nav_page %></a>
        % }
      % }
      % if ($next_page) {
        <a href="<%= url_with->query({page => $next_page}) %>">次へ</a>
      % }
      <a href="<%= url_with->query({page => $last_page}) %>">最後へ</a>
    </div>
  % }

$selfはMojolicious::Controllerです。appでMojoliciousを継承しているアプリケーションオブジェクトを取得します。dbiで、DBIx::Customオブジェクトを取得しています。

url_withとqueryへハッシュリファレンスを渡す方法を使用すると、現在のURLのクエリを維持しながら、ページ番号だけ置換することができます。

よくある質問

where句と組み合わせることはできますか?

はい、where句を組み合わせることができます。本のタイトルに、Perlを含むの例です。

my $where = [
  'title like :title', # where句
  {title => '%Perl%'}, # パラメーター
];
my $books = $dbi->model('book')->select(
  ['id', 'name'],
  append => "limit $offset, $count_per_page",
  where => $where
)->all;
my $total = $dbi->model('book')->select('count(*)', where => $where)->value;

URLの他のクエリ文字列をそのままにして、ページ番号だけを変えることはできますか?

ここで解説したサンプルは、ページ番号だけの変更に対応しています。

url_forではなくurl_withを使うこと。queryメソッドの引数にハッシュリファレンス「{page => $page}」を渡すことがポイントです。

ページネーションを部品化することはできますか?

はい、ページネーションは、上下に表示したり、他のページと共有したい場合がありますね。

そのような場合は、include機能を使うことができます。

テンプレートファイル「templates/include/pagenation.html.ep」を作成します。

stashを使ってpagerを受け取ります。

  <%
    my $pager = stash('pager');
    
    # 現在のページ
    my $page = $pager->current_page;
    
    # ページナビゲーションの個数を設定
    $pager->pages_per_navigation(10);
    
    # 前のページ
    my $previous_page = $pager->previous_page;
    
    # 次のページ
    my $next_page = $pager->next_page;
    
    # 最初のページ
    my $first_page = $pager->first_page;
    
    # 最後のページ
    my $last_page = $pager->last_page;

    # 周辺のページ番号を取得
    my @nav_pages = $pager->pages_in_navigation;
  %>
  % if ($first_page != $last_page) {
    <div class="pagenation">
      <a href="<%= url_with->query({page => undef}) %>">最初へ</a>
      % if ($previous_page) {
        <a href="<%= url_with->query({page => $previous_page}) %>">前へ</a>
      % }
      % for my $nav_page (@nav_pages) {
        % if ($nav_page eq $page) {
          <span><%= $nav_page %></a>
        % } else {
          <a href="<%= url_with->query({page => $nav_page}) %>"><%= $nav_page %></a>
        % }
      % }
      % if ($next_page) {
        <a href="<%= url_with->query({page => $next_page}) %>">次へ</a>
      % }
      <a href="<%= url_with->query({page => $last_page}) %>">最後へ</a>
    </div>
  % }

使う側は、以下のようにします。

%= include 'include/pagenation', pager => $pager;

関連情報