File Coverage

blib/lib/DBIx/Mint/Schema.pm
Criterion Covered Total %
statement 123 123 100.0
branch 24 26 92.3
condition 29 47 61.7
subroutine 19 19 100.0
pod 4 7 57.1
total 199 222 89.6


line stmt bran cond sub pod time code
1             package DBIx::Mint::Schema;
2              
3 15     15   18835 use DBIx::Mint::ResultSet;
  15         40  
  15         358  
4 15     15   5352 use DBIx::Mint::Schema::Class;
  15         32  
  15         357  
5 15     15   69 use Carp;
  15         15  
  15         600  
6 15     15   122 use v5.10;
  15         68  
  15         422  
7 15     15   54 use Moo;
  15         17  
  15         61  
8              
9             has classes => ( is => 'rw', default => sub {{}} );
10             has tables => ( is => 'rw', default => sub {{}} );
11              
12             sub instance {
13 9     9 0 16014 my ($class, $name) = @_;
14 9   50     63 $name //= '_DEFAULT';
15 9         44 return DBIx::Mint->instance($name)->schema;
16             }
17              
18             sub add_class {
19 31     31 1 167352 my $self = shift;
20 31         445 my $class = DBIx::Mint::Schema::Class->new(@_);
21 30         178 $self->classes->{$class->class} = $class;
22 30         114 $self->tables->{ $class->table} = $class;
23             }
24              
25             sub for_class {
26 148     148 0 557 my ($self, $class) = @_;
27 148         760 return $self->classes->{$class};
28             }
29              
30             sub for_table {
31 19     19 0 27 my ($self, $table) = @_;
32 19         386 return $self->tables->{$table};
33             }
34              
35             sub one_to_many {
36 2     2 1 15 my ($schema, %params) = @_;
37              
38 2   33     5 my $conditions = $params{ conditions } || croak "one_to_many: join conditions are required";
39 2   33     5 my $method = $params{ method } || croak "one_to_many: method name is required";
40 2   100     6 my $inv_method = $params{ inverse_method } || undef;
41 2   100     6 my $insert_into = $params{ insert_into } || undef;
42            
43 2         8 $schema->add_relationship(result_as => 'all', inv_result_as => 'single', %params);
44              
45 2         4 return 1;
46             }
47              
48             sub many_to_many {
49 2     2 1 1063 my ($schema, %params) = @_;
50            
51 2   33     7 my $conditions = $params{ conditions } || croak "many_to_many: join conditions are required";
52 2   33     6 my $method = $params{ method } || croak "many_to_many: method name is required";
53 2   50     4 my $inv_method = $params{ inverse_method } || undef;
54 2 100       178 croak "insert_into is not supported for many_to_many relationships" if $params{insert_into};
55            
56 1         5 $schema->add_relationship(result_as => 'all', inv_result_as => 'all', %params);
57              
58 1         3 return 1;
59             }
60              
61             sub add_relationship {
62 11     11 1 4273 my ($schema, %params) = @_;
63            
64             # Support for from_class, to_class alternative (mainly for one-to-one associations)
65 11 100 100     68 if ($params{from_class} && $params{conditions}) {
66 3         12 $params{conditions} = [ $params{from_class}, $params{conditions}, $params{to_class}];
67             }
68              
69 11 100 100     71 if ($params{from_class} && ! exists $params{conditions}) {
70 5         16 my $pk = $schema->for_class( $params{from_class} )->pk->[0];
71 5         28 $params{conditions} = [ $params{from_class}, { $pk => $params{to_field} }, $params{to_class} ];
72             }
73            
74            
75 11   33     33 my $conditions = $params{ conditions } || croak "add_relationship: join conditions are required";
76 11   33     37 my $method = $params{ method } || croak "add_relationship: method name is required";
77 11   100     41 my $inv_method = $params{ inverse_method } || undef;
78 11   100     39 my $insert_into = $params{ insert_into } || undef;
79 11   50     40 my $inv_insert_into = $params{ inv_insert_into} || undef;
80 11   50     30 my $result_as = $params{ result_as } || undef;
81 11   100     37 my $inv_result_as = $params{ inv_result_as } || undef;
82              
83             # Create method into $from_class
84 11         15 my $from_class = $conditions->[0];
85 11         83 my $rs = $schema->_build_rs(@$conditions);
86 11         43 $schema->_build_method($rs, $from_class, $method, $result_as);
87            
88             # Create method into $target_class
89 10 100       24 if (defined $inv_method) {
90 8 100       15 my @cond_copy = map { ref $_ ? { reverse %$_ } : $_ } reverse @$conditions;
  26         73  
91 8         21 my $target_class = $cond_copy[0];
92 8         32 my $inv_rs = $schema->_build_rs(@cond_copy);
93 8         32 $schema->_build_method($inv_rs, $target_class, $inv_method, $inv_result_as);
94             }
95            
96             # Create insert_into method
97 10 100       25 if (defined $insert_into) {
98 2         3 my $join_cond = $conditions->[1];
99 2         4 my $target_class = $conditions->[2];
100 2         4 $schema->_build_insert_into($from_class, $target_class, $insert_into, $join_cond);
101             }
102              
103 10         30 return 1;
104             }
105              
106             sub _build_rs {
107 19     19   32 my ($schema, @conditions) = @_;
108 19         105 my $from_class = shift @conditions;
109 19         28 my $from_table = 'me';
110 19         19 my $to_table;
111            
112 19         60 my $rs = DBIx::Mint::ResultSet->new( table => $schema->for_class( $from_class )->table );
113            
114 19         91 do {
115 21         24 my $from_to_fields = shift @conditions;
116 21         25 my $to_class = shift @conditions;
117 21   33     39 my $class_obj = $schema->for_class($to_class) || croak "Class $to_class has not been defined";
118 21         40 $to_table = $class_obj->table;
119 21         21 my %join_conditions;
120 21         67 while (my ($from, $to) = each %$from_to_fields) {
121 24         36 $from = "$from_table.$from";
122 24         36 $to = "$to_table.$to";
123 24         74 $join_conditions{$from} = $to;
124             }
125 21         62 $rs = $rs->inner_join( $to_table, \%join_conditions );
126 21         121 $from_table = $to_table;
127             }
128             while (@conditions);
129            
130 19         386 return $rs->select( $to_table . '.*')->set_target_class( $schema->for_table($to_table)->class );
131             }
132              
133             sub _build_method {
134 19     19   37 my ($schema, $rs, $class, $method, $result_as) = @_;
135            
136 19         18 my @pk = @{ $schema->for_class($class)->pk };
  19         30  
137              
138 19   100     45 $result_as //= 'resultset';
139 19         68 my %valid_results = (
140             resultset => 1,
141             single => 1,
142             all => 1,
143             as_iterator => 1,
144             as_sql => 1
145             );
146 19 100       210 croak "result_as option not recognized for $class\::$method: '$result_as'"
147             unless exists $valid_results{ $result_as };
148            
149             {
150 15     15   13430 no strict 'refs';
  15         23  
  15         2298  
  18         15  
151 18         125 *{$class . '::' . $method} = sub {
152 16     16   9942 my $self = shift;
153 16         22 my %conditions;
154 16         126 $conditions{"me.$_"} = $self->$_ foreach @pk;
155 16         435 my $rs_copy = $rs->search(\%conditions);
156 16 100       72 if ( $result_as eq 'single' ) {
    100          
    100          
    100          
157 5         21 return $rs_copy->single;
158             }
159             elsif ($result_as eq 'all') {
160 5         15 return $rs_copy->all;
161             }
162             elsif ($result_as eq 'as_iterator') {
163 2         48 return $rs_copy->as_iterator;
164             }
165             elsif ($result_as eq 'as_sql') {
166 1         4 return $rs_copy->select_sql;
167             }
168             else {
169 3         7 return $rs_copy;
170             }
171 18         69 };
172             }
173             }
174              
175              
176             sub _build_insert_into {
177 2     2   3 my ($schema, $class, $target, $method, $conditions) = @_;
178            
179 15     15   239 no strict 'refs';
  15         17  
  15         2786  
180 2         8 *{$class . '::' . $method} = sub {
181 1     1   9 my $self = shift;
182 1         2 my @copies;
183 1         2 foreach my $record (@_) {
184 3 50       6 croak "insert_into methods take hash references as input (while using $class" . "::$method)"
185             unless ref $record eq 'HASH';
186 3         8 while (my ($from_field, $to_field) = each %$conditions) {
187 3 50       8 croak $class . "::" . $method .": $from_field is not defined"
188             if !defined $self->{$from_field};
189 3         8 $record->{$to_field} = $self->{$from_field};
190             }
191 3         5 push @copies, $record;
192             }
193 1         9 return $target->insert(@copies);
194 2         8 };
195 2         2 return 1;
196             }
197              
198             1;
199              
200             =pod
201              
202             =head1 NAME
203              
204             DBIx::Mint::Schema - Class and relationship definitions for DBIx::Mint
205              
206             =head1 SYNOPSIS
207              
208             # Using the schema from the default Mint object:
209             my $schema = DBIx::Mint->instance->schema;
210              
211             # Using a named schema:
212             my $schema = DBIx::Mint::Schema->instance( 'other' );
213              
214             # which is the same as this:
215             my $mint = DBIx::Mint->instance('other');
216             my $schema = $mint->schema;
217            
218            
219             $schema->add_class(
220             class => 'Bloodbowl::Coach',
221             table => 'coaches',
222             pk => 'id',
223             auto_pk => 1
224             );
225            
226             $schema->one_to_many(
227             conditions =>
228             [ 'Bloodbowl::Team', { id => 'team' }, 'Bloodbowl::Player' ],
229             method => 'get_players',
230             inverse_method => 'get_team',
231             insert_into => 'add_player'
232             );
233            
234             $schema->many_to_many(
235             conditions => [ 'Bloodbowl::Player', { id => 'player'},
236             'Bloodbowl::PlayerSkills, { skill => 'skill' },
237             'Bloodbowl::Skill' ],
238             method => 'get_skills',
239             inverse_method => 'get_players'
240             );
241              
242             $schema->add_relationship(
243             conditions =>
244             ['Bloodbowl::Team', { id => 'team' }, 'Bloodbowl::Players'],
245             method => 'players_rs',
246             result_as => 'result_set'
247             );
248              
249             =head1 DESCRIPTION
250              
251             This module lets you declare the mapping between classes and database tables, and it creates methods that act on the relationships you define. It is an essential part of L.
252              
253             =head1 METHODS
254              
255             =head2 add_class
256              
257             Defines the mapping between a class and a database table. It expects the following arguments:
258              
259             =over
260              
261             =item class
262              
263             The name of the class. Required.
264              
265             =item table
266              
267             The name of the table it points to. Required.
268              
269             =item pk
270              
271             Defines the primary key in the database. It can be a single field name or an array reference of field names. Required.
272              
273             =item auto_pk
274              
275             Lets DBIx::Mint know that the pk is automatically generated by the database. It expects a boolean value. Optional; defaults to false.
276              
277             =item fields_not_in_db
278              
279             Receives an array ref of attributes of the given class which are not stored in the database. They will be removed from the data before inserting or updating it into the database.
280              
281             =back
282              
283             =head2 one_to_many
284              
285             Builds a one-to-many relationship between two classes. Internally, it is built using the method L, which builds closures that contain a L object to fetch related records and, optionally, an insert_into method. It expects the following parameters:
286              
287             =over
288              
289             =item conditions
290              
291             Defines both the classes that the relationship binds and the fields that are used to link them. These conditions are then used to build L joins.
292              
293             The attribute receives an array reference with the following format:
294              
295             [ 'Class::One', { from_field => 'to_field' }, 'Class::Many' ]
296              
297             one_to_many will insert a method into Class::One which will return the (many) related records of Class::Many, using from_field and to_field to link the classes.
298              
299             This parameter is required.
300              
301             =item method
302              
303             Defines the name of the method that is inserted into the 'one' class defined in C. This method will return a list of all the related records of the 'many' class, blessed. Required.
304              
305             =item inverse_method
306              
307             It creates a method in the 'many' side of the relationship that returns the related record from the 'one' side. The returned record is a blessed object. Optional.
308              
309             =item insert_into
310              
311             If present, this parameter defines the name of a method which is inserted into the 'one' class which allows it to insert related records into the 'many' class. It expects hash references as input. Note that they should have the same keys in order to benefit from a prepared insert statement.
312              
313             =back
314              
315             =head2 many_to_many
316              
317             Builds a many-to-many relationship between two classes. Internally, it is built using the method L, which builds closures that contain a L object to fetch related records. It expects the following parameters:
318              
319             =over
320              
321             =item conditions
322              
323             Defines the chain of classes that the relationship binds and the fields that are used to link them. These conditions are then used to build L joins.
324              
325             The attribute receives an array reference with the following format:
326              
327             [ 'Class::One', { from_field => 'to_field' },
328             'Class::Two', { from_two => 'to_three' },
329             'Class::Three' ]
330              
331             many_to_many will insert a method into Class::One which will return the (many) related records of Class::Three, joined through Class::Two. The size of the array can be arbitrarily long.
332              
333             This parameter is required.
334              
335             =item method
336              
337             Defines the name of the method that is inserted into the first class defined in C. This method will return a list of all the related records of the last class, blessed. Required.
338              
339             =item inverse_method
340              
341             It creates a method in the last class that returns a list of all the related records from the first. The records are blessed objects. Optional.
342              
343             =back
344              
345             =head2 add_relationship
346              
347             This method creates a one-to-one, one-to-many or many-to-many relationship and it allows you to define the returned form of the resulting records.
348              
349             =over
350              
351             =item conditions
352              
353             Same as many_to_many relationships.
354              
355             =item method, inverse_method
356              
357             These parameters receive the name of the methods that will be inserted into the first and last classes defined for the relationship. The difference with one_to_many and many_to_many is that, by default, you will get a L object. See result_as and inv_result_as for other options.
358              
359             =item insert_into
360              
361             Same as one_to_many. It will insert records into the second class you define which are related to the first class. In a many-to-many relationship that uses a single link class, this method will allow you to insert objects into the link class.
362              
363             =item result_as, inv_result_as
364              
365             These two parameters define the results that you will get from the created method and inverse_method. The allowed options are:
366              
367             =over
368              
369             =item resultset
370              
371             This is the default. The method will return a L object suitable for chaining conditions or paging. It offers the most flexibility.
372              
373             =item single
374              
375             Methods will return a single, blessed object from your set of results.
376              
377             =item all
378              
379             Methods will return all the related records from your set of results.
380              
381             =item as_iterator
382              
383             Methods will return a L object with an iterator to fetch one record at a time from your set of results. It is used as follows:
384              
385             my $rs = $obj->method;
386             while (my $record = $rs->next) {
387             say $record->name;
388             }
389              
390             =item as_sql
391              
392             This form will return the generated select SQL statement and the list of bind values. Useful for debugging.
393              
394             =back
395              
396             =back
397              
398             =head1 SEE ALSO
399              
400             This module is part of L.
401              
402             =head1 AUTHOR
403              
404             Julio Fraire,
405              
406             =head1 LICENCE AND COPYRIGHT
407              
408             Copyright (c) 2013, Julio Fraire. All rights reserved.
409              
410             =head1 LICENSE
411              
412             This module is free software; you can redistribute it and/or
413             modify it under the same terms as Perl itself. See L.
414              
415             This program is distributed in the hope that it will be useful,
416             but WITHOUT ANY WARRANTY; without even the implied warranty of
417             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
418              
419             =cut
420