DBIx::Skinnyを使った際のPaging方法考察

DBIx::Skinnyにはネイティブにpagingをしてくれる便利機能はありません。
(最近ないないばっかり言ってるな)


DBICとかだと$rs->pagerみたいにしてData::Pageのオブジェクトを返してくれるんですが、
Data::Pageのオブジェクトを作る際に、内部でcountを発行しています。
pagingするにはSQLにLIMIT/OFFSETをかけてると、思うのでLIMIT/OFFSETを掛けなかった際の
トータルな件数を取るためですね。


結構このcountが馬鹿にならないくらい内部で発行されることがあるのでSkinnyではあえてサポートしなかったです。


あと、独自にSQLを書かせる事をお題目にあげているので、
独自に書かれたSQLを内部でごちゃごちゃしてcount発行するとかヤッテラレナイてのもあります。


ただ、アプリを作ってる時にpagingは必須なのでどうすれば良いのかのサンプルをば。


一個目がMySQL限定のやり方です。

サンプルは
http://github.com/nekokak/p5-dbix-skinny-sample/tree/master/paging-mysql/
にあります。


MySQLの機能にSQL_CALC_FOUND_ROWS/FOUND_ROWS()というのがあるのでそれを使います。
参考:http://dev.mysql.com/doc/refman/4.1/ja/miscellaneous-functions.html

これを使えば、LIMIT/OFFSETを掛けない際に実際どれだけのレコード数を取るのかをGETできるので
こんな感じでやってやります。

package Demo::DBPager;
use strict;
use warnings;
use Data::Page;

sub users {
    my ($self, $db, $page) = @_;

    my $limit  = 2;
    my $offset = $limit*($page-1);

    my $itr = $db->search_by_sql(q{SELECT SQL_CALC_FOUND_ROWS * FROM user LIMIT ? OFFSET ?},[$limit, $offset]);
    my $rows = $db->search_by_sql('SELECT FOUND_ROWS() AS row')->first;

    my $pager = Data::Page->new(
        $rows->row, $limit, $page
    );
    return ($itr, $pager);
}

1;


呼び出し側はこんな感じ

my ($itr, $pager) = Demo::DBPager->users($db, 1);
warn Dumper $pager;
warn Dumper (map {$_->get_columns} $itr->all);

($itr, $pager) = Demo::DBPager->users($db, 2);
warn Dumper $pager;
warn Dumper (map {$_->get_columns} $itr->all);


内部で件数をカウントする分DBの負荷にはなるかと思いますが、自前でcountを取るよりは早いし負荷も抑えられるはずです。


これがMySQLの機能でやる方法。



次にRDBMSに依存しない形の方法を

サンプルは
http://github.com/nekokak/p5-dbix-skinny-sample/tree/master/paging/
ここにあります。


全部の件数を取得せずにLIMIT/OFFSETを掛けた時に次のレコードがあるかを調べることによって、
次のページがあるかを判定する方法になります。

ちょっとごちゃごちゃしてますが、
2件表示する場合、3件のデータを取れるかをためして、
3件あれば次のページがある、2件以下の場合は次のページはないよという方法。

package Demo::DBPager;
use strict;
use warnings;
use Data::Page;

sub users {
    my ($self, $db, $page) = @_;

    my $limit  = 2;
    my $offset = $limit*($page-1);

    my @rows = map { $_->get_columns } $db->search_by_sql(q{SELECT * FROM user LIMIT ? OFFSET ?},[($limit+1), $offset]);

    my $has_next = scalar(@rows) > $limit ? 1 : 0;
    if ($has_next) {
        pop @rows;
    }

    my $total_rows = ($limit * $page) + $has_next - (scalar(@rows) < $limit ? 1 : 0);
    my $pager = Data::Page->new(
        $total_rows,
        $limit,
        $page,
    );

    return (\@rows, $pager);
}

1;


使い方は特に変わらず

my ($rows, $pager) = Demo::DBPager->users($db, 1);
warn Dumper $pager;
warn Dumper $rows;

($rows, $pager) = Demo::DBPager->users($db, 2);
warn Dumper $pager;
warn Dumper $rows;

($rows, $pager) = Demo::DBPager->users($db, 3);
warn Dumper $pager;
warn Dumper $rows;

この方法の場合はRDBMSに依存せずに書くことができますが、
totalの件数が分からないので表示のさせ方がしょぼくなりますね。


まぁ、こんな感じでやればSkinnyでもページングさせることは可能というお話でした。