File Coverage

blib/lib/Mad/Mapper.pm
Criterion Covered Total %
statement 66 214 30.8
branch 12 78 15.3
condition 3 45 6.6
subroutine 25 50 50.0
pod 5 6 83.3
total 111 393 28.2


line stmt bran cond sub pod time code
1             package Mad::Mapper;
2              
3             =head1 NAME
4              
5             Mad::Mapper - Map Perl objects to PostgreSQL, MySQL or SQLite data
6              
7             =head1 VERSION
8              
9             0.06
10              
11             =head1 DESCRIPTION
12              
13             L is base class for objects that should be stored to the a
14             persistent SQL database. Currently the supported backends are L
15             L and L. These backends need to be installed
16             separately.
17              
18             $ cpanm Mad::Mapper
19             $ cpanm Mojo::Pg # Mad::Mapper now support postgres!
20              
21             THIS MODULE IS EXPERIMENTAL. It is in use in production though, so
22             big changes will not be made without extreme consideration.
23              
24             =head1 SYNOPSIS
25              
26             The synopsis is split into three parts: The two first is for model developers,
27             and the last is for the developer using the models.
28              
29             package MyApp::Model::User;
30             use Mad::Mapper -base;
31              
32             # Class attributes
33             col id => undef;
34             col email => '';
35              
36             See also L for more details and
37             L if you want more control.
38              
39             =head1 RELATIONSHIPS
40              
41             See L for example "has many" relationship.
42              
43             TODO: C and maybe C.
44              
45             =cut
46              
47 5     5   137450 use Mojo::Base -base;
  5         11  
  5         38  
48 5     5   4518 use Mojo::IOLoop;
  5         604895  
  5         30  
49 5     5   3655 use Mojo::JSON ();
  5         76202  
  5         143  
50 5     5   36 use Mojo::Loader 'load_class';
  5         9  
  5         242  
51 5     5   23 use Scalar::Util 'weaken';
  5         9  
  5         249  
52 5   50 5   24 use constant DEBUG => $ENV{MAD_DEBUG} || 0;
  5         9  
  5         6188  
53              
54             our $VERSION = '0.06';
55              
56             my (%COLUMNS, %LOADED, %PK);
57              
58             =head1 EXPORTED FUNCTIONS
59              
60             =head2 col
61              
62             Used to define a column. Follow the same rules as L.
63              
64             =head2 has
65              
66             has name => "Bruce";
67             has [qw(name email)];
68             has pet => sub { Cat->new };
69              
70             Same as L.
71              
72             =head2 pk
73              
74             Used to define a primary key. Follow the same rules as L.
75              
76             The primary key is used by default in L and L to update the
77             correct row. If omitted, the first L will act as primary key.
78              
79             Note that L is not returned by L.
80              
81             =head2 table
82              
83             Used to define a table name. The default is to use the last part of the class
84             name and add "s" at the end, unless it already has "s" at the end. Examples:
85              
86             .----------------------------.
87             | Class name | table |
88             |-------------------|--------|
89             | App::Model::User | users |
90             | App::Model::Users | users |
91             | App::Model::Group | groups |
92             '----------------------------'
93              
94             =head1 ATTRIBUTES
95              
96             =head2 db
97              
98             $db = $self->db;
99             $self->db($db_obj);
100              
101             Need to hold either a L or L object.
102              
103             =head2 in_storage
104              
105             $bool = $self->in_storage;
106             $self = $self->in_storage($bool);
107              
108             =cut
109              
110             has db => sub { die "'db' is required in constructor." };
111             has in_storage => 0;
112              
113             =head1 METHODS
114              
115             =head2 expand_sst
116              
117             ($sst, @args) = $self->expand_sst($sst, @args);
118              
119             Used to expand a given C<$sst> with variables defined by helpers.
120              
121             =over 4
122              
123             =item * %t
124              
125             Will be replaced by
. Example: "SELECT * FROM %t" becomes "SELECT * FROM users". 126               127             =item * %c 128               129             Will be replaced by L. Example: "name,email". 130               131             =item * %c= 132               133             Will be replaced by L assignment. Example: "name=?,email=?" 134               135             =item * %c? 136               137             Will be replaced by L placeholders. Example: "?,?,?" 138               139             =item * %pc 140               141             Include L in list of columns. Example: "id,name,email". 142               143             =item * \%c 144               145             Becomes a literal "%c". 146               147             =back 148               149             It is also possible to defined aliases for "%t", "%c", "%c=" and "%pc". Example: 150               151             %t.x = some_table as x 152             %c.x = x.col1 153               154             =cut 155               156             sub expand_sst { 157 0     0 1 0 my ($self, $sst, @args) = @_; 158 0         0 my $p; 159               160 0 0       0 $sst =~ s|(?columns}|ge;   0         0     0         0     0         0     0         0   161 0         0 $sst =~ s|(?columns}|ge;   0         0     0         0     0         0   162 0 0       0 $sst =~ s|(?columns}|ge;   0         0     0         0     0         0     0         0   163 0 0       0 $sst =~ s|(?pk, $self->columns}|ge;   0         0     0         0     0         0     0         0   164 0 0       0 $sst =~ s|(?table. ($1 ? " $1" : "")}|ge;   0         0     0         0   165 0         0 $sst =~ s|\\%|%|g; 166               167 0         0 return $sst, @args; 168             } 169               170             =head2 columns 171               172             @str = $self->columns; 173               174             Returns a list of columns, defined by L. 175               176             =head2 delete 177               178             $self = $self->delete; 179             $self = $self->delete(sub { my ($self, $err) = @_, ... }); 180               181             Will delete the object from database if L. 182               183             =cut 184               185             sub delete { 186 0     0 1 0 my $self = shift; 187 0 0       0 $self->_delete(@_) if $self->in_storage; 188 0         0 $self; 189             } 190               191             =head2 fresh 192               193             $self = $self->fresh; 194               195             Will mark the next relationship accessor to fetch new data from database, 196             instead of using the cached data on C<$self>. 197               198             =cut 199               200 0     0 1 0 sub fresh { $_[0]->{fresh}++; $_[0] }   0         0   201               202             =head2 load 203               204             $self = $self->load; 205             $self = $class->load(sub { my ($self, $err) = @_; }); 206               207             Used to fetch data from storage and update the object attributes. 208               209             =cut 210               211             sub load { 212 0     0 1 0 my $self = shift; 213 0         0 $self->_find(@_); 214 0         0 $self; 215             } 216               217             =head2 save 218               219             $self = $self->save; 220             $self = $self->save(sub { my ($self, $err) = @_, ... }); 221               222             Will update the object in database if L or insert it if not. 223               224             =cut 225               226             sub save { 227 0     0 1 0 my $self = shift; 228 0 0       0 $self->in_storage ? $self->_update(@_) : $self->_insert(@_); 229 0         0 $self; 230             } 231               232             =head2 import 233               234             Will set up the caller class with L functionality if "-base" 235             is given as argument. See L for example. 236               237             =cut 238               239             # Most of this code is copy/paste from Mojo::Base 240             sub import { 241 9     9   176 my $class = shift; 242 9 100       9346 return unless my $flag = shift; 243               244 5 50 0     18 if ($flag eq '-base') { $flag = $class }   5 0       11       0           245 0         0 elsif ($flag eq '-strict') { $flag = undef } 246             elsif ((my $file = $flag) && !$flag->can('new')) { 247 0         0 $file =~ s!::|'!/!g; 248 0         0 require "$file.pm"; 249             } 250               251 5 50       24 if ($flag) { 252 5         12 my $caller = caller; 253 5         25 my $table = lc +(split /::/, $caller)[-1]; 254 5         25 $table =~ s!s?$!s!; # user => users 255 5     11   35 Mojo::Util::monkey_patch($caller, col => sub { $caller->_define_col(@_) });   11     11   447   256 5 50   2   110 Mojo::Util::monkey_patch($caller, columns => sub { @{$COLUMNS{$caller} || []} });   2     2   7     2         33   257 5     1   98 Mojo::Util::monkey_patch($caller, has => sub { Mojo::Base::attr($caller, @_) });   1     1   47   258 5     8   96 Mojo::Util::monkey_patch($caller, has_many => sub { $caller->_define_has_many(@_) });   8     8   224   259             Mojo::Util::monkey_patch($caller, 260 5 100   14   97 pk => sub { return UNIVERSAL::isa($_[0], $caller) ? $PK{$caller} : $caller->_define_pk(@_) });   14     14   107205   261 5 100   6   93 Mojo::Util::monkey_patch($caller, table => sub { $table = $_[0] unless UNIVERSAL::isa($_[0], $caller); $table });   6     6   1832     6         17   262 5     5   28 no strict 'refs';   5         7     5         13165   263 5         79 push @{"${caller}::ISA"}, $flag;   5         65   264             } 265               266 5         109 $_->import for qw(strict warnings utf8); 267 5         4857 feature->import(':5.10'); 268             } 269               270             sub _delete { 271 0     0   0 my ($self, $cb) = @_; 272 0         0 my @sst = $self->_delete_sst; 273               274 0         0 warn "[Mad::Mapper::delete] ", Mojo::JSON::encode_json(\@sst), "\n" if DEBUG; 275               276 0 0       0 if ($cb) { 277 0         0 weaken $self; 278             $self->db->query( 279             @sst, 280             sub { 281 0     0   0 my ($db, $err, $res) = @_; 282 0         0 warn "[Mad::Mapper::delete] err=$err\n" if DEBUG and $err; 283 0 0       0 $self->in_storage(0) unless $err; 284 0         0 $self->$cb($err); 285             } 286 0         0 ); 287             } 288             else { 289 0         0 $self->db->query(@sst); 290 0         0 $self->in_storage(0); 291             } 292             } 293               294             sub _delete_sst { 295 0     0   0 my $self = shift; 296 0         0 my $pk = $self->_pk_or_first_column; 297               298 0         0 $self->expand_sst("DELETE FROM %t WHERE $pk=?"), $self->$pk; 299             } 300               301             sub _define_col { 302 11   33 11   43 my $class = ref($_[0]) || $_[0]; 303 11 50       14 push @{$COLUMNS{$class}}, ref $_[0] eq 'ARRAY' ? @{$_[1]} : $_[1];   11         43     0         0   304 11         36 Mojo::Base::attr(@_); 305             } 306               307             sub _define_has_many { 308 8     8   18 my ($class, $method, $related_class, $related_col) = @_; 309 8         37 my $pk = $class->_pk_or_first_column; 310 8         70 my $sst_method = $class->can("_has_many_${method}_sst"); 311               312             Mojo::Util::monkey_patch( 313             $class => $method => sub { 314 0 0   0   0 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;         0               0       315 0         0 my $self = shift; 316 0 0       0 my $err = $LOADED{$related_class}++ ? 0 : load_class $related_class; 317 0         0 my $fresh = delete $self->{fresh}; 318 0   0     0 my $ck = join ':', $method, grep { $_ // '' } @_;   0         0   319 0         0 my @sst; 320               321 0 0       0 die ref $err ? "Exception: $err" : "Could not find class $related_class!" if $err;     0           322               323             @sst 324 0 0       0 = $sst_method 325             ? $self->$sst_method($related_class, @_) 326             : $related_class->expand_sst("SELECT %pc FROM %t WHERE $related_col=?", $self->$pk); 327               328             warn sprintf "[Mad::Mapper::has_many::$method] %s\n", 329 0         0 (!$fresh and $self->{cache}{$ck}) ? 'CACHED' : Mojo::JSON::encode_json(\@sst) 330             if DEBUG; 331               332 0 0       0 if ($cb) { 333 0 0 0     0 if ($fresh or !$self->{cache}{$ck}) { 334             $self->db->query( 335             @sst, 336             sub { 337 0     0   0 my ($db, $err, $res) = @_; 338 0         0 warn "[Mad::Mapper::has_many::$method] err=$err\n" if DEBUG and $err; 339 0         0 $self->{cache}{$ck} = $res->hashes->map(sub { $related_class->new($_)->in_storage(1) });   0         0   340 0         0 $self->$cb($err, $self->{cache}{$ck}); 341             } 342 0         0 ); 343             } 344             else { 345 0         0 $self->$cb('', $self->{cache}{$ck}); 346             } 347 0         0 return $self; 348             } 349             else { 350 0 0       0 delete $self->{cache}{$ck} if $fresh; 351             return $self->{cache}{$ck} 352 0   0 0   0 ||= $self->db->query(@sst)->hashes->map(sub { $related_class->new($_)->in_storage(1) });   0         0   353             } 354             } 355 8         51 ); 356               357 8         140 my $add_method = "add_$method"; 358 8         35 $add_method =~ s!s?$!!; 359             Mojo::Util::monkey_patch( 360             $class => $add_method => sub { 361 0     0   0 my $self = shift;         0               0       362 0 0       0 my $err = $LOADED{$related_class}++ ? 0 : load_class $related_class; 363 0         0 $related_class->new(db => $self->db, @_, $related_col => $self->$pk); 364             } 365 8         104 ); 366             } 367               368             sub _define_pk { 369 5   33 5   29 my $class = ref($_[0]) || $_[0]; 370 5         16 $PK{$class} = $_[1]; 371 5         22 Mojo::Base::attr(@_); 372             } 373               374             sub _find { 375 0     0   0 my ($self, $cb) = @_; 376 0         0 my @sst = $self->_find_sst; 377               378 0         0 warn "[Mad::Mapper::find] ", Mojo::JSON::encode_json(\@sst), "\n" if DEBUG; 379 0 0       0 if ($cb) { 380 0         0 weaken $self; 381             $self->db->query( 382             @sst, 383             sub { 384 0     0   0 my ($db, $err, $res) = @_; 385 0         0 warn "[Mad::Mapper::find] err=$err\n" if DEBUG and $err; 386 0 0 0     0 $res = $err ? {} : $res->hash || {}; 387 0 0 0     0 $self->in_storage(1) if %$res and !$err; 388 0         0 $self->{$_} = $res->{$_} for keys %$res; 389 0         0 $self->$cb($err); 390             } 391 0         0 ); 392             } 393             else { 394 0   0     0 my $res = $self->db->query(@sst)->hash || {}; 395 0 0       0 $self->in_storage(1) if keys %$res; 396 0         0 $self->{$_} = $res->{$_} for keys %$res; 397             } 398             } 399               400             sub _find_sst { 401 0     0   0 my $self = shift; 402 0         0 my $pk = $self->_pk_or_first_column; 403               404 0         0 $self->expand_sst("SELECT %pc FROM %t WHERE $pk=?"), $self->$pk; 405             } 406               407             sub _insert { 408 0     0   0 my ($self, $cb) = @_; 409 0         0 my $pk = $self->_pk_or_first_column; 410 0         0 my $db = $self->db; 411 0         0 my @sst = $self->_insert_sst; 412               413 0         0 warn "[Mad::Mapper::insert] ", Mojo::JSON::encode_json(\@sst), "\n" if DEBUG; 414               415 0 0       0 if ($cb) { 416 0         0 weaken $self; 417             $db->query( 418             @sst, 419             sub { 420 0     0   0 my ($db, $err, $res) = @_; 421 0         0 warn "[Mad::Mapper::insert] err=$err\n" if DEBUG and $err; 422 0   0     0 $res = eval { $res->hash } || {}; 423               424 0 0       0 if ($pk) { 425 0   0     0 $res->{$pk} ||= $db->dbh->last_insert_id(undef, undef, $self->table, $self->pk); 426 0   0     0 $res->{$pk} ||= eval { $res->sth->mysql_insertid }; # can probably be removed   0         0   427             } 428               429 0 0       0 $self->in_storage(1) if keys %$res; 430 0         0 $self->$_($res->{$_}) for grep { $self->can($_) } keys %$res;   0         0   431 0         0 $self->$cb($err); 432             } 433 0         0 ); 434             } 435             else { 436 0         0 my $res = $db->query(@sst); 437 0   0     0 $res = eval { $res->hash } || {}; 438               439 0 0       0 if ($pk) { 440 0   0     0 $res->{$pk} ||= $db->dbh->last_insert_id(undef, undef, $self->table, $self->pk); 441 0   0     0 $res->{$pk} ||= eval { $res->sth->mysql_insertid } # can probably be removed;   0         0   442             } 443               444 0 0       0 $self->in_storage(1) if keys %$res; 445 0         0 $self->$_($res->{$_}) for grep { $self->can($_) } keys %$res; # used with Mojo::Pg and RETURNING   0         0   446             } 447             } 448               449             sub _insert_sst { 450 0     0   0 my $self = shift; 451 0         0 my $pk = $self->pk; 452 0         0 my $sql = "INSERT INTO %t (%c) VALUES (%c?)"; 453               454 0 0 0     0 $sql .= " RETURNING $pk" if $pk and UNIVERSAL::isa($self->db, 'Mojo::Pg::Database'); 455 0         0 $self->expand_sst($sql), map { $self->$_ } $self->columns;   0         0   456             } 457               458 8 50   8   21 sub _pk_or_first_column { $_[0]->pk || ($_[0]->columns)[0] } 459               460             sub _update { 461 0     0   0 my ($self, $cb) = @_; 462 0         0 my @sst = $self->_update_sst; 463               464 0         0 warn "[Mad::Mapper::update] ", Mojo::JSON::encode_json(\@sst), "\n" if DEBUG; 465               466 0 0       0 if ($cb) { 467 0         0 weaken $self; 468             $self->db->query( 469             @sst, 470             sub { 471 0     0   0 my ($db, $err, $res) = @_; 472 0         0 warn "[Mad::Mapper::update] err=$err\n" if DEBUG and $err; 473 0         0 $self->$cb($err); 474             } 475 0         0 ); 476             } 477             else { 478 0         0 $self->db->query(@sst); 479             } 480             } 481               482             sub _update_sst { 483 0     0   0 my $self = shift; 484 0         0 my $pk = $self->_pk_or_first_column; 485               486 0         0 $self->expand_sst("UPDATE %t SET %c= WHERE $pk=?"), (map { $self->$_ } $self->columns), $self->$pk;   0         0   487             } 488               489             sub TO_JSON { 490 1     1 0 2098 my $self = shift; 491 1         3 my $pk = $self->pk; 492 1 50       6 return {$pk ? ($pk => $self->$pk) : (), map { ($_ => $self->$_) } $self->columns};   3         18   493             } 494               495             =head1 COPYRIGHT AND LICENSE 496               497             Copyright (C) 2014, Jan Henning Thorsen 498               499             This program is free software, you can redistribute it and/or modify it under 500             the terms of the Artistic License version 2.0. 501               502             =head1 AUTHOR 503               504             Jan Henning Thorsen - C 505               506             =cut 507               508             1;