File Coverage

blib/lib/DBIx/Sunny/Schema.pm
Criterion Covered Total %
statement 85 220 38.6
branch 17 74 22.9
condition 5 18 27.7
subroutine 17 31 54.8
pod 10 12 83.3
total 134 355 37.7


line stmt bran cond sub pod time code
1             package DBIx::Sunny::Schema;
2              
3 1     1   85298 use strict;
  1         11  
  1         25  
4 1     1   4 use warnings;
  1         2  
  1         26  
5 1     1   4 use Carp qw/croak/;
  1         2  
  1         56  
6 1     1   358 use parent qw/Class::Data::Inheritable/;
  1         251  
  1         5  
7 1     1   1076 use Data::Validator;
  1         26204  
  1         34  
8 1     1   468 use DBIx::Sunny::Util qw/bind_and_execute expand_placeholder/;
  1         2  
  1         52  
9 1     1   374 use DBIx::TransactionManager;
  1         2929  
  1         28  
10 1     1   1234 use DBI qw/:sql_types/;
  1         14900  
  1         375  
11 1     1   11 use Scalar::Util qw/blessed/;
  1         1  
  1         61  
12 1     1   511 use SQL::Maker::SQLType qw/sql_type/;
  1         822  
  1         65  
13              
14             $Carp::Internal{"DBIx::Sunny::Schema"} = 1;
15              
16             use Class::Accessor::Lite 0.05 (
17 1         6 new => 1,
18             ro => [qw/dbh readonly/],
19 1     1   391 );
  1         1019  
20              
21             __PACKAGE__->mk_classdata( '__validators' );
22             __PACKAGE__->mk_classdata( '__deflaters' );
23              
24             sub fill_arrayref {
25 0     0 0 0 my $self = shift;
26 0         0 return expand_placeholder(@_);
27             }
28              
29             sub select_one {
30 2     2 1 9 my $class = shift;
31 2 50       5 if ( ref $class ) {
32 0         0 my ($query, @bind) = expand_placeholder(@_);
33 0         0 my $row = $class->dbh->selectrow_arrayref($query, {}, @bind);
34 0 0       0 return unless $row;
35 0         0 return $row->[0];
36             }
37 2         3 my @args = @_;
38             $class->__setup_accessor(
39             sub {
40 0     0   0 my $do_query = shift;
41 0         0 my $self = shift;
42 0         0 my ( $sth, $ret ) = $do_query->($self,@_);
43 0         0 my $row = $sth->fetchrow_arrayref;
44 0         0 $sth->finish;
45 0 0       0 return unless $row;
46 0         0 return $row->[0];
47             },
48             @args
49 2         9 );
50             }
51              
52             sub select_row {
53 2     2 1 27 my $class = shift;
54 2 50       4 if ( ref $class ) {
55 0         0 my ($query, @bind) = expand_placeholder(@_);
56 0         0 my $row = $class->dbh->selectrow_hashref($query, {}, @bind);
57 0 0       0 return unless $row;
58 0         0 return $row;
59             }
60 2         4 my $filter;
61 2 100 66     10 $filter = pop @_ if ref $_[-1] && ref $_[-1] eq 'CODE';
62 2         4 my @args = @_;
63             $class->__setup_accessor(
64             sub {
65 0     0   0 my $do_query = shift;
66 0         0 my $self = shift;
67 0         0 my ( $sth, $ret ) = $do_query->($self,@_);
68 0         0 my $row = $sth->fetchrow_hashref;
69 0         0 $sth->finish;
70 0 0       0 return unless $row;
71 0 0       0 if ( $filter ) {
72 0         0 $filter->($row, $self);
73             }
74 0         0 return $row;
75             },
76             @args
77 2         8 );
78             }
79              
80             sub select_all {
81 5     5 1 36 my $class = shift;
82 5 50       11 if ( ref $class ) {
83 0         0 my ($query, @bind) = expand_placeholder(@_);
84 0         0 my $rows = $class->dbh->selectall_arrayref($query, { Slice => {} }, @bind);
85 0         0 return $rows;
86             }
87 5         5 my $filter;
88 5 100 66     15 $filter = pop @_ if ref $_[-1] && ref $_[-1] eq 'CODE';
89 5         16 my @args = @_;
90             $class->__setup_accessor(
91             sub {
92 0     0   0 my $do_query = shift;
93 0         0 my $self = shift;
94 0         0 my ( $sth, $ret ) = $do_query->($self,@_);
95 0         0 my @rows;
96 0         0 while( my $row = $sth->fetchrow_hashref ) {
97 0 0       0 if ( $filter ) {
98 0         0 $filter->($row, $self);
99             }
100 0         0 push @rows, $row;
101             }
102 0         0 $sth->finish;
103 0         0 return \@rows;
104             },
105             @args
106 5         68 );
107             }
108              
109             sub query {
110 2     2 1 1717 my $class = shift;
111 2 50       6 if ( ref $class ) {
112 0         0 my ($query, @bind) = expand_placeholder(@_);
113 0 0       0 croak "couldnot use query for readonly database handler" if $class->readonly;
114 0         0 my $sth = $class->prepare($query);
115 0         0 return $sth->execute(@bind);
116             }
117 2         5 my @args = @_;
118             $class->__setup_accessor(
119             sub {
120 0     0   0 my $do_query = shift;
121 0         0 my $self = shift;
122 0 0       0 croak "couldnot use query for readonly database handler" if $self->readonly;
123 0         0 my ( $sth, $ret ) = $do_query->($self, @_);
124 0         0 $sth->finish;
125 0         0 return $ret;
126             },
127             @args
128 2         19 );
129             }
130              
131             sub args {
132 0     0 1 0 my $self = shift;
133              
134 0 0       0 my $class = ref $self ? ref $self : $self;
135 0         0 my $method = [caller(1)]->[3];
136              
137              
138 0         0 my $validators = $class->__validators;
139 0 0       0 if ( !$validators ) {
140 0         0 $validators = $class->__validators({});
141             }
142 0         0 my $deflaters = $class->__deflaters;
143 0 0       0 if ( !$deflaters ) {
144 0         0 $deflaters = $class->__deflaters({});
145             }
146              
147 0 0       0 if ( ! exists $validators->{$method} ) {
148 0         0 my @rules;
149             my %deflater;
150 0         0 while ( my ($name,$rule) = splice @_, 0, 2 ) {
151 0 0       0 $rule = ref $rule ? $rule : { isa => $rule };
152 0 0       0 if ( my $deflater = delete $rule->{deflater} ) {
153 0 0 0     0 croak("deflater must be CodeRef in rule:$name")
154             if ( !ref($deflater) || ref($deflater) ne 'CODE');
155 0         0 $deflater{$name} = $deflater;
156             }
157 0         0 push @rules, $name, $rule;
158             }
159 0         0 $deflaters->{$method} = \%deflater;
160 0         0 $validators->{$method} = Data::Validator->new(@rules);
161             }
162              
163 0         0 my @caller_args;
164             {
165 0         0 package DB;
166 0         0 () = caller(1);
167 0 0 0     0 shift @DB::args if $class eq ( ref($DB::args[0]) || $DB::args[0] );
168 0         0 @caller_args = @DB::args;
169             }
170 0         0 local $Carp::CarpLevel = 3;
171 0         0 local $Carp::Internal{'Data::Validator'} = 1;
172 0         0 my $result = $validators->{$method}->validate(@caller_args);
173              
174 0 0       0 if ( my @deflaters = keys %{$deflaters->{$method}} ) {
  0         0  
175 0         0 &Internals::SvREADONLY($result, 0);
176 0         0 for ( @deflaters ) {
177 0         0 $result->{$_} = $deflaters->{$method}->{$_}->($result->{$_});
178             }
179 0         0 &Internals::SvREADONLY($result, 1);
180             }
181 0         0 $result;
182             }
183              
184             sub __setup_accessor {
185 11     11   20 my $class = shift;
186 11         15 my $cb = shift;
187 11         12 my $method = shift;
188 11         13 my $query_tmpl = pop;
189              
190 11         21 my @rules;
191             my %deflater;
192 11         0 my @bind_keys;
193 11         28 while ( my ($name,$rule) = splice @_, 0, 2 ) {
194 8 100       19 $rule = ref $rule ? $rule : { isa => $rule };
195 8 100       16 if ( my $deflater = delete $rule->{deflater} ) {
196 2 50 33     10 croak("deflater must be CodeRef in rule:$name")
197             if ( !ref($deflater) || ref($deflater) ne 'CODE');
198 2         5 $deflater{$name} = $deflater;
199             }
200 8         11 push @bind_keys, $name;
201 8         19 push @rules, $name, $rule;
202             }
203 11         15 my $is_named_placeholder_query = 1;
204 11         16 for my $key (@bind_keys) {
205 6         11 my $metakey = quotemeta $key;
206 6 50       51 if ($query_tmpl !~ /:$metakey/) {
207 6         8 $is_named_placeholder_query = undef;
208 6         10 last;
209             }
210             }
211 11 100       17 if (!$is_named_placeholder_query) {
212 6         6 my $placeholder_num = 0;
213 6         25 $placeholder_num++ while $query_tmpl =~ m/\?/g;
214 6         7 my $bind_num = scalar @bind_keys;
215 6 50       10 if ($placeholder_num != $bind_num) {
216 0         0 croak "The number of placeholders($placeholder_num) and the number of arguments($bind_num) do not match in the query: $query_tmpl";
217             }
218             }
219              
220 11         72 my $validator = Data::Validator->new(@rules);
221              
222             my $do_query = sub {
223 0     0   0 my $self = shift;
224 0         0 local $Carp::Internal{'Data::Validator'} = 1;
225 0         0 my $args = do {
226 0         0 my $raw_args = $validator->validate(@_);
227 0         0 my $args = {};
228 0         0 for my $key ( @bind_keys ) {
229 0         0 my $type = $validator->find_rule($key)->{type};
230 0 0       0 if ( $type->is_a_type_of('ArrayRef') ) {
231 0         0 my $vals = [];
232 0         0 my $type_parameter_bind_type = $self->type2bind($type->type_parameter);
233 0         0 my @val = @{$raw_args->{$key}};
  0         0  
234 0         0 for my $val ( @val ) {
235 0 0       0 if ( $deflater{$key} ) {
236 0         0 $val = $deflater{$key}->($val);
237 0         0 $type_parameter_bind_type = undef;
238             }
239 0 0       0 push @$vals, $type_parameter_bind_type
240             ? sql_type(\$val, $type_parameter_bind_type)
241             : $val;
242             }
243 0         0 $args->{$key} = $vals;
244             }
245             else {
246 0         0 my $bind_type = $self->type2bind($type);
247 0         0 my $val = $raw_args->{$key};
248 0 0       0 if ( $deflater{$key} ) {
249 0         0 $val = $deflater{$key}->($val);
250 0         0 $bind_type = undef;
251             }
252 0 0       0 $args->{$key} = $bind_type
253             ? sql_type(\$val, $bind_type)
254             : $val;
255             }
256             }
257 0         0 $args;
258             };
259              
260 0         0 my $query = $query_tmpl;
261 0         0 my @bind_param;
262 0 0       0 if ($is_named_placeholder_query) {
263 0         0 ($query, my $bind_param) = SQL::NamedPlaceholder::bind_named($query, $args);
264 0         0 @bind_param = @$bind_param;
265             }
266             else {
267 0         0 my @bind = map { $args->{$_} } @bind_keys;
  0         0  
268 0         0 ($query, @bind_param) = expand_placeholder($query, @bind);
269             }
270 0         0 my $sth = $self->dbh->prepare_cached($query);
271 0         0 my $ret = bind_and_execute($sth, @bind_param);
272 0         0 return ( $sth, $ret );
273 11         820 };
274              
275             {
276 1     1   1579 no strict 'refs';
  1         1  
  1         309  
  11         22  
277 11         81 *{"$class\::$method"} = sub {
278 0     0     $cb->( $do_query, @_ );
279 11         30 };
280             }
281             }
282              
283             sub type2bind {
284 0     0 0   my $self = shift;
285 0           my $type = shift;
286 0 0         return $type->is_a_type_of('Int') ? SQL_INTEGER :
    0          
287             $type->is_a_type_of('Num') ? SQL_FLOAT : undef;
288             }
289              
290             sub txn_scope {
291 0     0 1   my $self = shift;
292 0   0       $self->{__txn} ||= DBIx::TransactionManager->new($self->dbh);
293 0           $self->{__txn}->txn_scope( caller => [caller(0)] );
294             }
295              
296             sub prepare {
297 0     0 1   my $self = shift;
298 0           $self->dbh->prepare(@_);
299             }
300              
301             sub do {
302 0     0 1   my $self = shift;
303 0           $self->dbh->do(@_);
304             }
305              
306             sub func {
307 0     0 1   my $self = shift;
308 0           $self->dbh->func(@_);
309             }
310              
311             sub last_insert_id {
312 0     0 1   my $self = shift;
313 0           $self->dbh->last_insert_id(@_);
314             }
315              
316              
317             1;
318             __END__
319              
320             =encoding utf-8
321              
322             =head1 NAME
323              
324             DBIx::Sunny::Schema - SQL Class Builder
325              
326             =head1 SYNOPSIS
327              
328             package MyProj::Data::DB;
329            
330             use parent qw/DBIx::Sunny::Schema/;
331             use Mouse::Util::TypeConstraints;
332            
333             subtype 'Uint'
334             => as 'Int'
335             => where { $_ >= 0 };
336            
337             subtype 'Natural'
338             => as 'Int'
339             => where { $_ > 0 };
340            
341             enum 'Flag' => qw/1 0/;
342            
343             no Mouse::Util::TypeConstraints;
344              
345             __PACKAGE__->select_one(
346             'max_id',
347             'SELECT max(id) FROM member'
348             );
349            
350             __PACKAGE__->select_row(
351             'member',
352             id => { isa => 'Natural' }
353             'SELECT * FROM member WHERE id=?',
354             );
355            
356             __PACAKGE__->select_all(
357             'recent_article',
358             public => { isa => 'Flag', default => 1 },
359             offset => { isa => 'Uint', default => 0 },
360             limit => { isa => 'Uint', default => 10 },
361             'SELECT * FROM articles WHERE public=? ORDER BY created_on LIMIT ?,?',
362             );
363              
364             __PACAKGE__->select_all(
365             'recent_article',
366             id => { isa => 'ArrayRef[Uint]' },
367             'SELECT * FROM articles WHERE id IN(?)',
368             );
369             # This method rewrites query like 'id IN (?,?..)' with Array's value number
370            
371             __PACKAGE__->query(
372             'add_article',
373             member_id => 'Natural',
374             flag => { isa => 'Flag', default => '1' },
375             subject => 'Str',
376             body => 'Str',
377             created_on => { isa => .. },
378             <<SQL);
379             INSERT INTO articles (member_id, public, subject, body, created_on)
380             VALUES ( ?, ?, ?, ?, ?)',
381             SQL
382            
383             __PACKAGE__->select_one(
384             'article_count_by_member',
385             member_id => 'Natural',
386             'SELECT COUNT(*) FROM articles WHERE member_id = ?',
387             );
388            
389             __PACKAGE__->query(
390             'update_member_article_count',
391             article_count => 'Uint',
392             id => 'Natural'
393             'UPDATE member SET article_count = ? WHERE id = ?',
394             );
395            
396             ...
397            
398             package main;
399            
400             use MyProj::Data::DB;
401             use DBIx::Sunny;
402            
403             my $dbh = DBIx::Sunny->connect(...);
404             my $db = MyProj::Data::DB->new(dbh=>$dbh,readonly=>0);
405            
406             my $max = $db->max_id;
407             my $member_hashref = $db->member(id=>100);
408             # my $member = $db->member(id=>'abc'); #validator error
409            
410             my $article_arrayref = $db->recent_article( offset => 10 );
411            
412             {
413             my $txn = $db->dbh->txn_scope;
414             $db->add_article(
415             member_id => $id,
416             subject => $subject,
417             body => $body,
418             created_on =>
419             );
420             my $last_insert_id = $db->dbh->last_insert_id;
421             my $count = $db->article_count_by_member( id => $id );
422             $db->update_member_article_count(
423             article_count => $count,
424             id => $id
425             );
426             $txn->commit;
427             }
428            
429             =head1 DESCRIPTION
430              
431             =head1 BUILDER CLASS METHODS
432              
433             =over 4
434              
435             =item C<< __PACKAGE__->select_one( $method_name, @validators, $sql ); >>
436              
437             build a select_one method named $method_name with validator. validators arguments are passed for Data::Validator. you can use Mouse's type constraint. Type constraint are also used for SQL's bind type determination.
438              
439             =item C<< __PACKAGE__->select_row( $method_name, @validators, $sql, [\&filter] ); >>
440              
441             build a select_row method named $method_name with validator. If a last argument is CodeRef, this CodeRef will be applied for a result row.
442              
443             =item C<< __PACKAGE__->select_all( $method_name, @validators, $sql, [\&filter] ); >>
444              
445             build a select_all method named $method_name with validator. If a last argument is CodeRef, this CodeRef will be applied for all result row.
446              
447             =item C<< __PACKAGE__->query( $method_name, @validators, $sql ); >>
448              
449             build a query method named $method_name with validator.
450              
451             =back
452              
453             =head1 FILTERING and DEFLATING
454              
455             =over 4
456              
457             =item FILTERING
458              
459             If you passed CodeRef to builder, this CodeRef will be applied for results.
460              
461             __PACAKGE__->select_all(
462             'recent_article',
463             limit => { isa => 'Uint', default => 10 },
464             'SELECT * FROM articles WHERE ORDER BY created_on LIMIT ?',
465             sub {
466             my ($row,$self)= @_;
467             $row->{created_on} = DateTime::Format::MySQL->parse_datetime($row->{created_on});
468             $row->{created_on}->set_time_zone("Asia/Tokyo");
469             }
470             );
471              
472             Second argument of filter CodeRef is instance object of your SQL class.
473              
474             =item DEFLATING
475              
476             If you want to deflate argument before execute SQL, you can it with adding deflater argument to validator rule.
477              
478             __PACKAGE__->query(
479             'add_article',
480             subject => 'Str',
481             body => 'Str',
482             created_on => { isa => 'DateTime', deflater => sub { shift->strftime('%Y-%m-%d %H:%M:%S') },
483             <<SQL);
484             INSERT INTO articles (subject, body, created_on)
485             VALUES ( ?, ?, ?)',
486             SQL
487              
488             =back
489              
490             =head1 METHODS
491              
492             =over 4
493              
494             =item C<< new({ dbh => DBI, readonly => ENUM(0,1) ) >> :DBIx::Sunny::Schema
495              
496             create instance of schema. if C<readonly> is true, query method's will raise exception.
497              
498             =item C<dbh> :DBI
499              
500             C<readonly> accessor for DBI database handler.
501              
502             =item C<select_one($query, @bind)> :Str
503              
504             Shortcut for prepare, execute and fetchrow_arrayref->[0]
505              
506             =item C<select_row($query, @bind)> :HashRef
507              
508             Shortcut for prepare, execute and fetchrow_hashref
509              
510             =item C<select_all($query, @bind)> :ArrayRef[HashRef]
511              
512             Shortcut for prepare, execute and selectall_arrayref(.., { Slice => {} }, ..)
513              
514             =item C<query($query, @bind)> :Str
515              
516             Shortcut for prepare, execute.
517              
518             =item C<txn_scope()> :DBIx::TransactionManager::Guard
519              
520             return DBIx::TransactionManager::Guard object
521              
522             =item C<do(@args)> :Str
523              
524             Shortcut for C<< $self->dbh->do() >>
525              
526             =item C<prepare(@args)> :DBI::st
527              
528             Shortcut for C<< $self->dbh->prepare() >>
529              
530             =item C<func(@args)> :Str
531              
532             Shortcut for C<< $self->dbh->func() >>
533              
534             =item C<last_insert_id(@args)> :Str
535              
536             Shortcut for C<< $self->dbh->last_insert_id() >>
537              
538             =item C<args(@rule)> :HashRef
539              
540             Shortcut for using Data::Validator. Optional deflater arguments can be used.
541             Data::Validator instance will cache at first time.
542              
543             sub retrieve_user {
544             my $self = shift;
545             my $args = $self->args(
546             id => 'Int',
547             created_on => {
548             isa => 'DateTime',
549             deflater => sub { shift->strftime('%Y-%m-%d %H:%M:%S')
550             },
551             );
552             $arg->{id} ...
553             }
554              
555             C<$args> is validated arguments. C<@_> is not needed.
556              
557             =back
558              
559             =head1 AUTHOR
560              
561             Masahiro Nagano E<lt>kazeburo KZBRKZBR@ gmail.comE<gt>
562              
563             =head1 SEE ALSO
564              
565             C<DBI>, C<DBIx::TransactionManager>, C<Data::Validator>
566              
567             =head1 LICENSE
568              
569             This library is free software; you can redistribute it and/or modify
570             it under the same terms as Perl itself.
571              
572             =cut