File Coverage

blib/lib/DBIx/Quick.pm
Criterion Covered Total %
statement 188 228 82.4
branch 32 48 66.6
condition 6 11 54.5
subroutine 31 38 81.5
pod n/a
total 257 325 79.0


line stmt bran cond sub pod time code
1             package DBIx::Quick;
2              
3 2     2   288403 use v5.16.3;
  2         8  
4 2     2   9 use strict;
  2         6  
  2         52  
5 2     2   8 use warnings;
  2         4  
  2         134  
6              
7 2     2   1017 use Import::Into;
  2         5820  
  2         71  
8 2     2   12 use Data::Dumper;
  2         5  
  2         130  
9 2     2   1653 use SQL::Abstract::More;
  2         94846  
  2         11  
10              
11             my %SEARCHABLE_FIELDS;
12             my %TABLES;
13             my %COLUMNS;
14             my %FIELDS;
15             my %FIXED;
16             my %PRIMARY_KEYS;
17             my %CONVERTERS;
18              
19             our $VERSION = "0.9";
20              
21             sub import {
22 6     6   801 my $caller = caller;
23 6         155 my $caller_instance = "${caller}::Instance";
24 6         1078 require Moo;
25 6         9450 Moo->import::into($caller);
26 6         6354 Moo->import::into($caller_instance);
27             {
28 2     2   39951 no strict 'refs';
  2         9  
  2         6891  
  6         4755  
29              
30 6         42 *{"${caller}::field"} = sub {
31 18     18   101421 _field_sub( $caller, $caller_instance, @_ );
32 6         54 };
33              
34 6         31 *{"${caller}::table"} = sub {
35 6     6   275795 my $tablename = shift;
36 6         22 $TABLES{$caller} = $tablename;
37 6         33 };
38              
39 6         25 *{"${caller}::fix"} = sub {
40             die "To fix the object $caller fill the table name"
41 6 50   6   1080 if !$TABLES{$caller};
42             die "A pk field is needed to fix $caller"
43 6 50       18 if !$PRIMARY_KEYS{$caller};
44 6         38 $caller_instance->can('has')->( dbh => ( is => 'ro' ) );
45 6         1928 $caller->can('with')->('DBIx::Quick::Role');
46             {
47 6         3834 *{"${caller_instance}::fetch_again"} = sub {
  6         48  
48 1     1   4690 my ($self) = @_;
49             my ($instance) = @{
50 1         3 $caller->new( dbh => $self->dbh )->search(
51             $PRIMARY_KEYS{$caller} =>
52 1         8 $self->can( $PRIMARY_KEYS{$caller} )->($self)
53             )
54             };
55 1         101 return $instance;
56 6         38 };
57             }
58 6         24 $FIXED{$caller} = 1;
59 6         41 };
60              
61 6         27 *{"${caller}::instance_has"} = sub {
62 3     3   896 my (@args) = @_;
63 3         32 $caller_instance->can('has')->(@args);
64 6         29 };
65              
66 6         30 *{"${caller}::instance_sub"} = sub {
67 3 50   3   2716 if ( @_ != 2 ) {
68 0         0 die 'Wrong number of arguments, expected .';
69             }
70 3         9 my ( $name, $code ) = @_;
71 3 50       12 if ( 'CODE' ne ref $code ) {
72 0         0 die 'Expected coderef in the second argument';
73             }
74 3         5 *{"${caller_instance}::${name}"} = $code;
  3         25  
75 6         60 };
76              
77 6         25 *{"${caller}::insert"} = sub {
78 3     3   5900 my ( $self, $instance ) = @_;
79 3         16 _insert( $caller, $self, $instance );
80 6         43 };
81              
82 6         23 *{"${caller}::update"} = sub {
83 0     0   0 my ( $self, $instance, @to_update ) = @_;
84 0         0 _update( $caller, $self, $instance, @to_update );
85 6         26 };
86              
87 6         47 *{"${caller}::delete"} = sub {
88 0     0   0 my ( $self, $instance ) = @_;
89 0         0 _delete( $caller, $self, $instance );
90 6         30 };
91              
92 6         40 *{"${caller}::search"} = sub {
93 7     7   4236 my ( $self, %search_params ) = @_;
94 7         29 _search( $caller, $self, %search_params );
95 6         22 };
96              
97 6         7435 *{"${caller}::free_search"} = sub {
98 1     1   2718 my ( $self, %search_params ) = @_;
99 1         7 _advanced_search( $caller, $self, %search_params );
100             }
101 6         22 }
102             }
103              
104             sub _delete {
105 0     0   0 my ( $caller, $self, $instance ) = @_;
106 0         0 _check_fixed($caller);
107 0         0 my $dbh = $self->dbh;
108 0         0 my $sqla = SQL::Abstract::More->new;
109             my ( $sql, @bind ) = $sqla->delete(
110             -from => $TABLES{$caller},
111             -where => {
112             $PRIMARY_KEYS{$caller} =>
113 0         0 $instance->can( $PRIMARY_KEYS{$caller} )->($instance),
114             }
115             );
116 0         0 $dbh->do( $sql, undef, @bind );
117             }
118              
119             sub _insert {
120 3     3   11 my ( $caller, $self, $instance ) = @_;
121 3         14 _check_fixed($caller);
122 3         16 my $dbh = $self->dbh;
123 3         70 my $sqla = SQL::Abstract::More->new;
124             my ( $sql, @bind ) = $sqla->insert(
125 3         1198 -into => $TABLES{$caller},
126             -values => _filter_undef( _values_from_instance( $caller, $instance ) ),
127             );
128 3         2017 my $sth = $dbh->prepare($sql);
129 3         602 $sqla->bind_params($sth, @bind);
130 3         112 return $sth->execute;
131             }
132              
133             sub _filter_undef {
134 3     3   7 my $values = shift;
135 3         5 my %final_hash;
136 3         12 for my $key ( keys %$values ) {
137 10 100       25 next if !defined $values->{$key};
138 7         57 $final_hash{$key} = $values->{$key};
139             }
140 3         29 return \%final_hash;
141             }
142              
143             sub _update {
144 0     0   0 my ( $caller, $self, $instance, @values_to_update ) = @_;
145 0         0 _check_fixed($caller);
146 0         0 my $pk_col = _get_primary_key_column($caller);
147 0         0 my $pk_field = $PRIMARY_KEYS{$caller};
148 0         0 my $dbh = $self->dbh;
149 0         0 my $sqla = SQL::Abstract::More->new;
150             my ( $sql, @bind ) = $sqla->update(
151 0         0 -table => $TABLES{$caller},
152             -set => _filter_values(
153             $caller, _values_from_instance( $caller, $instance ),
154             @values_to_update
155             ),
156             -where => {
157             $pk_col => $instance->can($pk_field)->($instance),
158             }
159             );
160 0         0 my $sth = $dbh->prepare($sql);
161 0         0 $sqla->bind_params($sth, @bind);
162 0         0 return $sth->execute;
163             }
164              
165             sub _filter_values {
166 0     0   0 my ( $caller, $values, @only_if_present_here ) = @_;
167 0         0 my %final_values;
168 0         0 for my $key ( keys %$values ) {
169 0 0       0 if (
170 0     0   0 !List::Util::any { $_ eq $FIELDS{$caller}{$key} }
171             @only_if_present_here
172             )
173             {
174 0         0 next;
175             }
176 0         0 $final_values{$key} = $values->{$key};
177             }
178 0         0 return \%final_values;
179             }
180              
181             sub _get_primary_key_column {
182 0     0   0 my ($caller) = @_;
183 0         0 return $COLUMNS{$caller}{ $PRIMARY_KEYS{$caller} };
184             }
185              
186             sub _values_from_instance {
187 3     3   10 my ( $caller, $instance ) = @_;
188 3         5 my %columns = %{ $COLUMNS{$caller} };
  3         23  
189 3         7 my %final_hash;
190 3         52 for my $field ( keys %columns ) {
191 10         77 my $value = $instance->can($field)->($instance);
192 10 100       46 if ( defined $CONVERTERS{$caller}{$field} ) {
193 1         6 $value = $CONVERTERS{$caller}{$field}->to_db($value);
194             }
195 10         42 $final_hash{ $columns{$field} } = $value;
196             }
197 3         28 return \%final_hash;
198             }
199              
200             sub _check_fixed {
201 11     11   22 my ($caller) = @_;
202 11 50       46 die "$caller must be fixed before using it!" if !$FIXED{$caller};
203             }
204              
205             sub _advanced_search {
206 1     1   4 my ( $caller, $object, %search_params ) = @_;
207 1         4 _check_fixed($caller);
208 1         4 my $dbh = $object->dbh;
209 1         8 my $sqla = SQL::Abstract::More->new;
210             my @columns =
211 1         5 sort { $a cmp $b } map { $TABLES{$caller} . '.' . $COLUMNS{$caller}{$_} }
  2         9  
212 1         249 keys %{ $COLUMNS{$caller} };
  1         4  
213 1 50       7 if ( defined delete $search_params{'-from'} ) {
214 0         0 warn '-from not supported in free search use -join';
215             }
216 1 50       4 if ( defined delete $search_params{'-columns'} ) {
217 0         0 warn '-columns not supported in free search, use a normal'
218             . ' query with dbh instead, this returns rows of the DAO'
219             . ' with SQL::Abstract::More you can have all this module querying features.';
220             }
221 1   50     4 my $join = delete $search_params{'-join'} // [];
222 1 50       9 if ( 'ARRAY' ne ref $join ) {
223 0         0 die '-join must be an arrayref';
224             }
225 1         2 my @joins = @{$join};
  1         4  
226             my ( $sql, @bind ) = $sqla->select(
227             -columns => [@columns],
228 1         8 -from => [ -join => $TABLES{$caller}, (@joins) ],
229             %search_params,
230             );
231 2         131 return [ map { _row_to_instance( $caller, $object, $_ ) }
232 1         1698 @{ $dbh->selectall_arrayref( $sql, { Slice => {} }, @bind ) } ];
  1         9  
233             }
234              
235             sub _search {
236 7     7   19 my ( $caller, $object, %search_params ) = @_;
237 7         23 _check_fixed($caller);
238 7         23 my $dbh = $object->dbh;
239 7         41 my $sqla = SQL::Abstract::More->new;
240             my @columns =
241 23         66 sort { $a cmp $b } map { $TABLES{$caller} . '.' . $COLUMNS{$caller}{$_} }
  22         92  
242 7         1557 keys %{ $COLUMNS{$caller} };
  7         27  
243 7         16 my %final_search;
244 7         31 for my $param_key ( keys %search_params ) {
245 7         18 my $column = $COLUMNS{$caller}{$param_key};
246 7 50       20 if ( !defined $column ) {
247 0         0 die "$param_key in $caller doesn't exist";
248             }
249 7 100       24 if ( !defined $SEARCHABLE_FIELDS{$caller}{$param_key} ) {
250 1         8 die "$param_key in $caller is not searchable";
251             }
252 6         22 $final_search{$column} = $search_params{$param_key};
253             }
254             my ( $sql, @bind ) = $sqla->select(
255             -columns => [ @columns, ],
256 6 50       63 -from => $TABLES{$caller},
257             (
258             (%final_search)
259             ? ( -where => { %final_search, } )
260             : ()
261             )
262             );
263 7         2345 return [ map { _row_to_instance( $caller, $object, $_ ) }
264 6         3843 @{ $dbh->selectall_arrayref( $sql, { Slice => {} }, @bind ) } ];
  6         68  
265             }
266              
267             sub _row_to_instance {
268 9     9   23 my ( $caller, $object, $row ) = @_;
269 9         17 my %final_hash;
270 9         29 for my $column ( keys %$row ) {
271 26         52 my $field = $FIELDS{$caller}{$column};
272 26         41 my $value = $row->{$column};
273 26 100       71 if ( defined $CONVERTERS{$caller}{$field} ) {
274 1         40 $value = $CONVERTERS{$caller}{$field}->from_db($value);
275             }
276 26         65 $final_hash{$field} = $value;
277             }
278 9         46 return ( $caller . '::Instance' )->new( %final_hash, dbh => $object->dbh );
279             }
280              
281             sub _field_sub {
282 18     18   60 my ( $caller, $caller_instance, $field, @args ) = @_;
283 18         58 my %params = @args;
284 18         34 my $searchable = delete $params{search};
285 18         28 my $fk = delete $params{fk};
286 18   66     90 my $column = delete $params{column} // $field;
287 18         30 my $pk = delete $params{pk};
288 18         27 my $converter = delete $params{converter};
289 18 100       38 if ( defined $converter ) {
290 1 50       3 if ( !eval { $converter->does('DBIx::Quick::Converter') } ) {
  1         8  
291 0         0 die
292             "$field converter must implement the DBIx::Quick::Converter Role and be a Moo class";
293             }
294 1         77 $CONVERTERS{$caller}{$field} = $converter;
295             }
296 18         42 $COLUMNS{$caller}{$field} = $column;
297 18         37 $FIELDS{$caller}{$column} = $field;
298              
299 18 100       39 if ($pk) {
300 6         12 $PRIMARY_KEYS{$caller} = $field;
301             }
302 18         48 _parse_fk( $caller, $caller_instance, $field, $fk );
303 18         49 _mark_searchable( $caller, $searchable, $field );
304 18         63 _create_instance_attribute( $caller_instance, $field, %params );
305             }
306              
307             sub _create_instance_attribute {
308 18     18   46 my ( $caller_instance, $field, %args ) = @_;
309 18         156 $caller_instance->can('has')->( $field, %args );
310             }
311              
312             sub _parse_fk {
313 18     18   41 my ( $caller, $caller_instance, $field, $fk ) = @_;
314 18 50 33     55 if ( defined $fk && ( 'ARRAY' ne ref $fk || 3 > scalar @$fk ) ) {
      66        
315 0         0 die
316             "${caller}::${field} fk parameter must be an arrayref containing []";
317             }
318 18 100       68 if ( defined $fk ) {
319             my (
320 3         8 $remote_object, $remote_attr,
321             $remote_object_name_in_this_object,
322             $this_object_name_in_the_remote_object
323             ) = @$fk;
324 2     2   24 no strict 'refs';
  2         4  
  2         901  
325 3 100       8 if ( defined $this_object_name_in_the_remote_object ) {
326              
327             # One to one doesn't require this thingy.
328 2         15 *{ $remote_object
329             . '::Instance::'
330             . $this_object_name_in_the_remote_object } = sub {
331 1     1   1526 my ($self) = shift;
332 1 50       6 if ( !defined $self->dbh ) {
333 0         0 die 'This is not an object found by the ORM';
334             }
335 1         7 return $caller->new( dbh => $self->dbh )
336             ->search( $field => $self->can($remote_attr)->($self) );
337 2         13 };
338             }
339 3         15 *{ $caller_instance . '::' . $remote_object_name_in_this_object } =
340             sub {
341 1     1   3909 my ($self) = shift;
342 1 50       47 if ( !defined $self->dbh ) {
343 0         0 die 'This is not an object found by the ORM';
344             }
345 1         19 return $remote_object->new( dbh => $self->dbh )
346             ->search( $remote_attr => $self->can($field)->($self) );
347              
348 3         13 };
349 3         8 $SEARCHABLE_FIELDS{$caller}{$field} = 1;
350             }
351             }
352              
353             sub _mark_searchable {
354 18     18   38 my ( $caller, $searchable, $field ) = @_;
355 18 100       37 if ($searchable) {
356 14         30 $SEARCHABLE_FIELDS{$caller}{$field} = 1;
357             }
358             }
359             1;
360              
361             =pod
362              
363             =encoding utf-8
364              
365             =head1 NAME
366              
367             DBIx::Quick - Object Relational Mapping for the lazy programmer
368              
369             =head1 SYNOPSIS
370              
371             package MyApp::DAO::Users;
372            
373             use strict;
374             use warnings;
375            
376             use DBIx::Quick;
377              
378             table 'users';
379              
380             has dbh => (is => 'ro', required => 1);
381              
382             field id => (is => 'ro', search => 1, pk => 1);
383             field username => (is => 'rw', search => 1, required => 1, column => 'user_name');
384             field id_address => (is => 'rw', search => 1, fk => ['MyApp::DAO::Addresses', 'id', 'addresses', 'users']);
385             field timestamp => (is => 'rw', search => 1, converter => MyApp::DB::Converters::DateTime->new);
386              
387             fix;
388              
389             And elsewhere:
390              
391             my $user = MyApp::DAO::Users::Instance->new(username => 'lazybastard', id_address => 5);
392             my $dao = MyApp::DAO::Users->new(dbh => DBI->connect(...));
393             $dao->insert($user)
394             ($user) = @{$dao->search(username => 'lazybastard')};
395             $user->username('lazyandproductive');
396             $dao->update($user, 'username');
397             $user = $user->fetch_again;
398             $dao->delete($user);
399              
400             =head1 DESCRIPTION
401              
402             L is the needed bridge between L and your database, you create DAO objects in a similar fashion to L and those objects auto-create
403             the corresponding instances under the same package plus ::Instance, importing this module becomes your package into a L class and the created
404             class is also a L one.
405              
406             Many times writing object to relational database mapping you find yourself having to repeat the same information once and once again which becomes
407             tiring for the developer, Models and DAO are created in a single step in a single file to prevent this, but they remain completely separate classes,
408             methods are provided to take full advantage of the separation.
409              
410             The L syntax also provides shorter code overall.
411              
412             This module is preliminar, meaning the syntax is probably not the definitive one, if you are a programmer who wants to spend less effort into
413             making full blown applications feel free to join the development with suggestions or patches.
414              
415             If you are needing too fancy autocomplete or templates just to be productive maybe you instead need L.
416              
417             To check an example project that uses this code you can check L.
418              
419             =head1 DAO DECLARATIONS
420              
421             While declaring a L mapping you can use the following subs autoimported into your package namespace.
422              
423             =head2 table
424              
425             table 'transactions';
426              
427             Specify the table this DAO maps to.
428              
429             =head2 field
430              
431             field id => (is => 'ro', pk => 1, search => 1);
432             field amount => (is => 'ro', required => 1, search => 1, column => 'amnt');
433             field tax => (is => 'ro');
434             field id_user => (is => 'ro', required => 1, search => 1, fk => ['MyApp::DAO::Users', 'id', 'users', 'transactions']);
435              
436             field is the equivalent to L C sub implementing all its options (useful or not) to represent a column into the table.
437              
438             It provides the following extra options:
439              
440             =head3 pk
441              
442             Defines the primary key if sent a trueish value
443              
444             =head3 search
445              
446             Marks this column as searchable for the generated search method.
447              
448             =head3 column
449              
450             Allows to change the destination column by default it would be called as the field itself.
451              
452             =head3 fk
453              
454             Takes four arguments: The destination class, the destination field, the method to represent in our model the remote class and optionally the remote instance method to represent our own class.
455              
456             =head3 converter
457              
458             See L
459              
460             =head2 fix
461              
462             Ensures the class is ready to be used and marks the class as ready.
463              
464             =head2 instance_sub
465              
466             instance_sub uppercase_username => sub {
467             my $self = shift;
468             return uc($self->username);
469             };
470              
471             and later:
472              
473             $user->uppercase_username;
474              
475             Declares a subroutine to be added to the generated ::Instance Object.
476              
477             =head2 instance_has
478              
479             instance_has cache => (is => 'lazy');
480             instance_sub _build_cache => sub {
481             my $redis = Redis->new;
482             return $redis;
483             };
484              
485             Makes a L attribute available to the ::Instance Object with the same syntax than L has.
486              
487             =head2 dbh
488              
489             You must declare a dbh method or a L attribute returning a DBI connection.
490              
491             =head1 DAO METHODS
492              
493             =head2 search
494              
495             my @transactions = @{$dao->search(
496             # SQL::Abstract::More where syntax using field names instead of columns
497             amnt => { '>', 10000 },
498             tax => 21,
499             )}
500              
501             Searchs ::Instance Objects in the table using L where syntax but replacing the column names by field names.
502              
503             =head2 free_search
504              
505             my @transactions = @{$dao->free_search(
506             -join => [
507             'users.id=transactions.id_user users',
508             ],
509             -where => {
510             'users.surname' => {-like => 'Gar%'},
511             },
512             )};
513              
514             Searchs ::Instance Objects in the table using all the syntax of L select, columns are the real columns not fields.
515              
516             -columns and -from are not allowed to be used. -from should be substituted by -join and -columns is not needed.
517              
518             =head2 insert
519              
520             $dao->insert(MyApp::DAO::Users::Instance->new(username => 'ent'));
521              
522             Inserts a row in the table, doesn't return the inserted field. You can use UUIDs or other known unique attributes of the table to search for
523             the inserted object.
524              
525             =head2 update
526              
527             $user->username('X');
528             $user->surname('González');
529             $dao->update($user, 'username', 'surname');
530              
531             Takes an instance and a list of the fields that should be updated in db with the instance data for that row, updates them and doesn't have a
532             meaningful return. Searches the object to update by the primary key.
533              
534             =head2 delete
535              
536             $dao->delete($user);
537              
538             Vanishes the instance of the database. Searches by the primary key.
539              
540             =head1 INSTANCE SUBS
541              
542             =head2 fetch_again
543              
544             $user = $user->fetch_again;
545              
546             Get remote updates the ::Instance object may have.
547              
548             =head2 dbh
549              
550             $user->dbh($dbh);
551              
552             Sets a database to be used in the constructor of the corresponding DAO while doing fetch_again.
553              
554             =head1 BUGS AND LIMITATIONS
555              
556             Every DAO/Instance must be associated directly with a table, if you need something extra, the good old and reliable L is
557             enough to you.
558              
559             Errors must be improved to allow users to debug easier faulty code.
560              
561             API is not stable since this program is so early in its development that I do not know if incorrect assumptions or bad design is hiding here.
562              
563             No many to many easy wrapper, use free_search, I could not come up with something that would be better than directly creating your own
564             queries with free_search.
565              
566             =head1 AUTHOR
567              
568             SERGIOXZ - Sergio Iglesias
569              
570             =head1 CONTRIBUTORS
571              
572             SERGIOXZ - Sergio Iglesias
573              
574             =head1 COPYRIGHT
575              
576             Copyright © Sergio Iglesias (2025)
577              
578             =head1 LICENSE
579              
580             This library is free software and may be distributed under the same terms
581             as perl itself. See L.
582              
583             =cut