Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

add Plugin::Pager::Count and Plugin::Pager::Any #100

Open
wants to merge 2 commits into from

2 participants

@ktat

COUNT(*) を使ったPagerと、Pagerのクラスを選べるクラスの追加です。同じアプリケーションで全てのPagerが一緒の実装である理由もないので、選べて良いかなと思います。重い部分のみ独自のPagerを使うといったようなケースもあると思いますので。

ただ、これが問題ないなら、Pager::Any は、現在のPlugin::Pagerに実装して、今のPlugin::Pagerの実装をPlugin::Pager::Simple(適切な名前が分かりませんが) とかにしちゃったほうが良いような気もしますが。デフォルトのページャをPager::Simple(仮) にしておけば、互換性で問題は出ないので。

@tokuhirom
Collaborator

Any ってのは名前がよくない気がしますす。あと、そういう場合には load_plugin の引数で名前かえればいいだけかと。

@tokuhirom
Collaborator

あと、単に COUNT(*) つけた場合、うまくいかないクエリのパターンがあって、たとえば HAVING とかのときにうまくいかないとかが DBIC であった気がしていて、そういう想定外のものがはいらないかを考慮しないといけないなあ、という印象です。

@ktat

alias で名前変えるので十分ですかね。

COUNT(*)は、HAVINGだけだと大丈夫だと思うのですが、GROUP BYでうまく行きませんので、GROUP BY 付いている場合は、エラーとかにする処理を入れた方が良いですかね。

@ktat

alias がちょっと嫌なのは、人によって何の名前付けるか分からないところかなぁ...。

@tokuhirom
Collaborator

であれば、たとえば、search_with_pager 以外のデフォルトのメソッド名を各プラグインにふるとかの方が筋がよいようにおもいます。

@ktat

なるほど。
ただ、search_with_pager は最後にloadしたものになっちゃいますね(今もそうですが)。
これと関係ないですが、load_plugin時に定義済みの名前が使われた場合に、warning を出すようにした方が良いかも?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 29, 2012
  1. @ktat
  2. @ktat

    fix test file name in comment

    ktat authored
This page is out of date. Refresh to see the latest.
View
5 MANIFEST
@@ -17,6 +17,8 @@ lib/Teng/Plugin/Count.pm
lib/Teng/Plugin/FindOrCreate.pm
lib/Teng/Plugin/Lookup.pm
lib/Teng/Plugin/Pager.pm
+lib/Teng/Plugin/Pager/Count.pm
+lib/Teng/Plugin/Pager/Any.pm
lib/Teng/Plugin/Pager/MySQLFoundRows.pm
lib/Teng/Plugin/Replace.pm
lib/Teng/Plugin/SingleBySQL.pm
@@ -56,6 +58,8 @@ t/001_basic/030_reconnect_ping.t
t/001_basic/032_reconnect_no_ping.t
t/001_basic/033_load_plugin_with_args.t
t/001_basic/034_execute.t
+t/001_basic/035_pager_count.t
+t/001_basic/036_pager_any.t
t/002_common/000_new.t
t/002_common/001_insert.t
t/002_common/002_update.t
@@ -102,6 +106,7 @@ t/lib/Mock/BasicBindColumn/Schema.pm
t/lib/Mock/Inflate.pm
t/lib/Mock/Inflate/Name.pm
t/lib/Mock/Inflate/Schema.pm
+t/lib/Mock/Pager.pm
t/lib/MyGuard.pm
t/lib/Teng/Plugin/ArgsTest.pm
t/lib/TengTest.pm
View
127 lib/Teng/Plugin/Pager/Any.pm
@@ -0,0 +1,127 @@
+package Teng::Plugin::Pager::Any;
+
+use strict;
+use warnings;
+use Class::Load ();
+
+our @EXPORT = qw/search_with_pager pager_class/;
+my %pager_class;
+
+sub init {
+ my ($pkg, $class, $opt) = @_;
+ if (my @classes = @{$opt->{pager_classes} || [qw/Pager Count MySQLFoundRows/] }) {
+ $class->pager_class($classes[0]);
+ foreach my $class (@classes) {
+ if ($class =~s{^\+}{}) {
+ Class::Load::load_class($class);
+ } elsif (not $class or $class eq 'Pager') {
+ Class::Load::load_class('Teng::Plugin::Pager');
+ } else {
+ Class::Load::load_class('Teng::Plugin::Pager::' . $class);
+ }
+ }
+ }
+}
+
+sub search_with_pager {
+ my ($self, $table_name, $where, $opt) = @_;
+
+ my $full_pager_class = 'Teng::Plugin::Pager';
+ if (my $pager_class = delete $opt->{pager_class} || $self->pager_class) {
+ if ($pager_class =~ s{^\+}{}) {
+ $full_pager_class = $pager_class;
+ } elsif ($pager_class ne 'Pager') {
+ Carp::croak("Don't use 'Any' as pager_class.") if $pager_class eq 'Any';
+
+ $full_pager_class .= '::' . $pager_class;
+ }
+ }
+ my $pager_method = $full_pager_class . '::search_with_pager';
+
+ $self->$pager_method($table_name, $where, $opt);
+}
+
+sub pager_class {
+ my $class = ref($_[0]) ? ref($_[0]) : $_[0];
+ $pager_class{$class} = $_[1] if @_ > 1;
+ $pager_class{$class} || '';
+}
+
+1;
+__END__
+
+=for test_synopsis
+
+my ($dbh, $c);
+
+=head1 NAME
+
+Teng::Plugin::Pager::Any - enable to choose Pager class for search_with_pager
+
+=head1 SYNOPSIS
+
+ package MyApp::DB;
+ use parent qw/Teng/;
+ __PACKAGE__->load_plugin('Pager::Any');
+
+ package main;
+ my $db = MyApp::DB->new(dbh => $dbh);
+ my $page = $c->req->param('page') || 1;
+ my ($rows, $pager) = $db->search_with_pager('user' => {type => 3}, {page => $page, rows => 5, pager_class => 'Count'});
+
+When you want to specify Pager classes:
+
+ __PACKAGE__->load_plugin('Pager::Any', {pager_classes => ['Pager', 'Count', '+YourClass::Pager']});
+
+=head1 DESCRIPTION
+
+This is a helper to use pagination class.
+
+=head1 load_plugin OPTION
+
+=over 4
+
+=item pager_classes
+
+ __PACKAGE__->load_plugin('Pager::Any', {pager_classes => ['Pager', 'Count']});
+
+This option is to choose pager classes and set first passed class as a defualt pager class.
+If you don't pass this option, automatically load Pager, Pager::Count and Pager::MySQLFoundRows.
+
+If you want to load your own pager class, add C<+> to the class name:
+
+ __PACKAGE__->load_plugin('Pager::Any', {pager_classes => ['Pager', 'Count', '+YourClass::Pager']});
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item my (\@rows, $pager) = $db->search_with_pager($table_name, \%where, \%opts)
+
+Select from database with pagination.
+
+The arguments are mostly same as C<$db->search()>. But three additional options are available.
+
+=over 4
+
+=item $opts->{page}
+
+Current page number.
+
+=item $opts->{rows}
+
+The number of entries per page.
+
+=item $opts->{pager_class}
+
+Pager class you want to use. default is Teng::Plugin::Pager or first class in pager_classes load_plugin option.
+
+=back
+
+=item $db->pager_class($pager_class);
+
+set default pager class.
+
+=back
View
115 lib/Teng/Plugin/Pager/Count.pm
@@ -0,0 +1,115 @@
+package Teng::Plugin::Pager::Count;
+
+use strict;
+use warnings;
+use Data::Page;
+
+our @EXPORT = qw/search_with_pager/;
+
+sub search_with_pager {
+ my ($self, $table_name, $where, $opt) = @_;
+
+ my $table = $self->schema->get_table($table_name) or Carp::croak("'$table_name' is unknown table");
+
+ my $page = $opt->{page};
+ my $rows = $opt->{rows};
+
+ my ($count_sql, @count_binds) = $self->sql_builder->select(
+ $table_name,
+ [\'COUNT(*)'],
+ $where,
+ $opt,
+ );
+
+ my $columns = $opt->{'+columns'}
+ ? [@{$table->{columns}}, @{$opt->{'+columns'}}]
+ : ($opt->{columns} || $table->{columns})
+ ;
+
+ my ($sql, @binds) = $self->sql_builder->select(
+ $table_name,
+ $columns,
+ $where,
+ +{
+ %$opt,
+ limit => $rows,
+ offset => $rows*($page-1),
+ }
+ );
+ my $count_sth = $self->dbh->prepare($count_sql) or Carp::croak $self->dbh->errstr;
+ $count_sth->execute(@count_binds) or Carp::croak $self->dbh->errstr;
+ my $total_entries = $count_sth->fetchrow_arrayref->[0];
+
+ my $sth = $self->dbh->prepare($sql) or Carp::croak $self->dbh->errstr;
+ $sth->execute(@binds) or Carp::croak $self->dbh->errstr;
+
+ my $itr = Teng::Iterator->new(
+ teng => $self,
+ sth => $sth,
+ sql => $sql,
+ row_class => $self->schema->get_row_class($table_name),
+ table => $table,
+ table_name => $table_name,
+ suppress_object_creation => $self->suppress_row_objects,
+ );
+
+ my $pager = Data::Page->new();
+ $pager->entries_per_page($rows);
+ $pager->current_page($page);
+ $pager->total_entries($total_entries);
+
+ return ([$itr->all], $pager);
+}
+
+1;
+__END__
+
+=for test_synopsis
+my ($c, $dbh);
+
+=head1 NAME
+
+Teng::Plugin::Pager::Count - Paginate with COUNT(*)
+
+=head1 SYNOPSIS
+
+ package MyApp::DB;
+ use parent qw/Teng/;
+ __PACKAGE__->load_plugin('Pager::Count');
+
+ package main;
+ my $db = MyApp::DB->new(dbh => $dbh);
+ my $page = $c->req->param('page') || 1;
+ my ($rows, $pager) = $db->search_with_pager('user' => {type => 3}, {page => $page, rows => 5});
+
+=head1 DESCRIPTION
+
+This is a helper class for pagination. This helper only supports B<MySQL>.
+Since this plugin uses COUNT(*) for calculate total entries.
+
+=head1 METHODS
+
+=over 4
+
+=item my (\@rows, $pager) = $db->search_with_pager($table, \%where, \%opts);
+
+Select from database with pagination.
+
+The arguments are mostly same as C<$db->search()>. But two additional options are available.
+
+=over 4
+
+=item $opts->{page}
+
+Current page number.
+
+=item $opts->{rows}
+
+The number of entries per page.
+
+=back
+
+This method returns ArrayRef[Teng::Row] and instance of L<Teng::Plugin::Pager::Page>.
+
+=back
+
View
59 t/001_basic/035_pager_count.t
@@ -0,0 +1,59 @@
+use t::Utils;
+use Test::More;
+use Mock::Basic;
+
+my $dbh = t::Utils->setup_dbh;
+my $db = Mock::Basic->new({dbh => $dbh});
+$db->setup_test_db;
+Mock::Basic->load_plugin('Pager::Count');
+
+for my $i (1..32) {
+ $db->insert(mock_basic => { id => $i, name => 'name_'.$i });
+}
+
+subtest 'simple' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {rows => 3, page => 1});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ is $pager->previous_page, undef;
+};
+
+subtest 'last' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {rows => 3, page => 11});
+ is join(',', map { $_->id } @$rows), '31,32';
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 11;
+ is $pager->next_page, undef, 'next_page';
+ is $pager->previous_page, 10;
+};
+
+subtest 'simple_with_columns' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {columns => [qw/id/], rows => 3, page => 1});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is_deeply $rows->[0]->get_columns, +{ id => 1 };
+ is_deeply $rows->[1]->get_columns, +{ id => 2 };
+ is_deeply $rows->[2]->get_columns, +{ id => 3 };
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ is $pager->previous_page, undef;
+};
+subtest 'simple_with_+columns' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {'+columns' => [\'id+20 as calc'], rows => 3, page => 1});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is join(',', map { $_->calc } @$rows), '21,22,23';
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ is $pager->previous_page, undef;
+};
+
+done_testing;
+
+
View
144 t/001_basic/036_pager_any.t
@@ -0,0 +1,144 @@
+use lib qw(t/lib);
+use t::Utils;
+use Test::More;
+use Mock::Basic;
+
+my $dbh = t::Utils->setup_dbh;
+my $db = Mock::Basic->new({dbh => $dbh});
+$db->setup_test_db;
+Mock::Basic->load_plugin('Pager::Any', {pager_classes => ['Pager', 'Count', '+Mock::Pager']});
+
+for my $i (1..32) {
+ $db->insert(mock_basic => { id => $i, name => 'name_'.$i });
+}
+
+my %pager_option = (
+ '' => [{}, {pager_class => 'Count'}],
+ 'Pager' => [{}, {pager_class => 'Count'}],
+ 'Count' => [{pager_class => 'Pager'}, {}],
+ '+Mock::Pager' => [{pager_class => 'Pager'}, {}],
+ );
+
+my $pager_options;
+sub simple_pager_test {
+ my ($db, $pager_class, $pager_options) = @_;
+
+ # copy from 001_basic/025_pager.t
+ subtest 'simple' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {rows => 3, page => 1, %{$pager_options->[0]}});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is $pager->entries_per_page(), 3;
+ is $pager->entries_on_this_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ ok $pager->has_next, 'has_next';
+ is $pager->prev_page, undef;
+ };
+
+ subtest 'last' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {rows => 3, page => 11, %{$pager_options->[0]}});
+ is join(',', map { $_->id } @$rows), '31,32';
+ is $pager->entries_per_page(), 3;
+ is $pager->entries_on_this_page(), 2;
+ is $pager->current_page(), 11;
+ is $pager->next_page, undef, 'next_page';
+ ok !$pager->has_next, 'has_next';
+ is $pager->prev_page, 10;
+ };
+
+ subtest 'simple_with_columns' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {columns => [qw/id/], rows => 3, page => 1, %{$pager_options->[0]}});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is_deeply $rows->[0]->get_columns, +{ id => 1 };
+ is_deeply $rows->[1]->get_columns, +{ id => 2 };
+ is_deeply $rows->[2]->get_columns, +{ id => 3 };
+ is $pager->entries_per_page(), 3;
+ is $pager->entries_on_this_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ ok $pager->has_next, 'has_next';
+ is $pager->prev_page, undef;
+ };
+ subtest 'simple_with_+columns' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {'+columns' => [\'id+20 as calc'], rows => 3, page => 1, %{$pager_options->[0]}});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is join(',', map { $_->calc } @$rows), '21,22,23';
+ is $pager->entries_per_page(), 3;
+ is $pager->entries_on_this_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ ok $pager->has_next, 'has_next';
+ is $pager->prev_page, undef;
+ };
+}
+
+sub total_pager_test {
+ my ($db, $pager_class, $pager_options) = @_;
+
+ # copy from 035_pager_count.t
+ subtest 'simple' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {rows => 3, page => 1, %{$pager_options->[1]}});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ is $pager->previous_page, undef;
+ };
+
+ subtest 'last' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {rows => 3, page => 11, %{$pager_options->[1]}});
+ is join(',', map { $_->id } @$rows), '31,32';
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 11;
+ is $pager->next_page, undef, 'next_page';
+ is $pager->previous_page, 10;
+ };
+
+ subtest 'simple_with_columns' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {columns => [qw/id/], rows => 3, page => 1, %{$pager_options->[1]}});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is_deeply $rows->[0]->get_columns, +{ id => 1 };
+ is_deeply $rows->[1]->get_columns, +{ id => 2 };
+ is_deeply $rows->[2]->get_columns, +{ id => 3 };
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ is $pager->previous_page, undef;
+ };
+ subtest 'simple_with_+columns' => sub {
+ my ($rows, $pager) = $db->search_with_pager(mock_basic => {}, {'+columns' => [\'id+20 as calc'], rows => 3, page => 1, %{$pager_options->[1]}});
+ is join(',', map { $_->id } @$rows), '1,2,3';
+ is join(',', map { $_->calc } @$rows), '21,22,23';
+ is $pager->total_entries(), 32;
+ is $pager->entries_per_page(), 3;
+ is $pager->current_page(), 1;
+ is $pager->next_page, 2, 'next_page';
+ is $pager->previous_page, undef;
+ };
+}
+
+foreach my $pager_class ('', 'Pager', 'Count', '+Mock::Pager') {
+ my $pager_options = $pager_option{$pager_class};
+
+ if ($pager_class) {
+ $db->pager_class($pager_class);
+ }
+
+ simple_pager_test($db, $pager_class, $pager_options);
+ total_pager_test($db, $pager_class, $pager_options);
+
+}
+
+is_deeply [values %Mock::Pager::cache], [32];
+
+# default pager class is changed
+Mock::Basic->load_plugin('Pager::Any', {pager_classes => ['+Mock::Pager']});
+undef %Mock::Pager::cache;
+total_pager_test($db, '', [{}, {}]);
+is_deeply [values %Mock::Pager::cache], [32];
+
+done_testing;
+
View
69 t/lib/Mock/Pager.pm
@@ -0,0 +1,69 @@
+package Mock::Pager;
+
+use strict;
+use warnings;
+use Data::Page;
+
+our @EXPORT = qw/search_with_pager/;
+our %cache;
+
+sub search_with_pager {
+ my ($self, $table_name, $where, $opt) = @_;
+
+ my $table = $self->schema->get_table($table_name) or Carp::croak("'$table_name' is unknown table");
+
+ my $page = $opt->{page};
+ my $rows = $opt->{rows};
+
+ my ($count_sql, @count_binds) = $self->sql_builder->select(
+ $table_name,
+ [\'count(*)'],
+ $where,
+ $opt,
+ );
+
+ my $columns = $opt->{'+columns'}
+ ? [@{$table->{columns}}, @{$opt->{'+columns'}}]
+ : ($opt->{columns} || $table->{columns})
+ ;
+
+ my ($sql, @binds) = $self->sql_builder->select(
+ $table_name,
+ $columns,
+ $where,
+ +{
+ %$opt,
+ limit => $rows,
+ offset => $rows*($page-1),
+ }
+ );
+ my $total_entries = $cache{$count_sql};
+ if (not $total_entries) {
+ my $count_sth = $self->dbh->prepare($count_sql) or Carp::croak $self->dbh->errstr;
+
+ $count_sth->execute(@count_binds) or Carp::croak $self->dbh->errstr;
+ $cache{$count_sql} = $total_entries = $count_sth->fetchrow_arrayref->[0];
+ }
+
+ my $sth = $self->dbh->prepare($sql) or Carp::croak $self->dbh->errstr;
+ $sth->execute(@binds) or Carp::croak $self->dbh->errstr;
+
+ my $itr = Teng::Iterator->new(
+ teng => $self,
+ sth => $sth,
+ sql => $sql,
+ row_class => $self->schema->get_row_class($table_name),
+ table => $table,
+ table_name => $table_name,
+ suppress_object_creation => $self->suppress_row_objects,
+ );
+
+ my $pager = Data::Page->new();
+ $pager->entries_per_page($rows);
+ $pager->current_page($page);
+ $pager->total_entries($total_entries);
+
+ return ([$itr->all], $pager);
+}
+
+1;
Something went wrong with that request. Please try again.