File Coverage

blib/lib/Model/Envoy/Storage/DBIC.pm
Criterion Covered Total %
statement 130 139 93.5
branch 32 42 76.1
condition 18 33 54.5
subroutine 22 23 95.6
pod 7 8 87.5
total 209 245 85.3


line stmt bran cond sub pod time code
1             package Model::Envoy::Storage::DBIC;
2              
3             our $VERSION = '0.5.3';
4              
5 10     10   6099 use Moose;
  10         26  
  10         77  
6 10     10   67409 use Scalar::Util 'blessed';
  10         22  
  10         588  
7 10     10   4770 use MooseX::ClassAttribute;
  10         778332  
  10         48  
8              
9             extends 'Model::Envoy::Storage';
10              
11             =head1 Model::Envoy::Storage::DBIC
12              
13             A Moose Role that adds a DBIx::Class persistence layer to your Moose class
14              
15             =head2 Configuration
16              
17             with 'Model::Envoy' => { storage => {
18             'DBIC' => {
19             schema => sub {
20             ... connect to database here ...
21             }
22             }
23             } };
24              
25             The only configuration option for this plugin is a 'schema' method that returns a
26             C<DBIx::Class:Schema> based object with an open connection to the database. This method
27             will be passed a reference to your class as its only argument.
28              
29             =head3 C<dbic()>
30              
31             This is a method you will need to implement in each C<Model::Envoy> object
32             that use this storage plugin. It should return the name of the DBIx::Class ResultClass
33             that your Model uses for database storage.
34              
35             =head2 Traits
36              
37             This role implements one trait:
38              
39             =head3 DBIC
40              
41             Marking an attribute on your object with the 'DBIC' trait tells this role that it is
42             backed by a DBIx::Class ResultClass column of the same name. It also allows for
43             a few custom options you can apply to that attribute:
44              
45             =over
46              
47             =item primary_key => 1
48              
49             Indicates that this attribute corresponds to the primary key for the database record
50              
51             =item rel => 'rel_type'
52              
53             Indicates that the attribute is a relationship to another model or a list of models. Possible
54             values for this option are
55              
56             =over
57              
58             =item belongs_to
59              
60             =item has_many
61              
62             =item many_to_many
63              
64             =back
65              
66             =item mm_rel => 'bridge_name'
67              
68             For many-to-many relationships it is necessary to indicate what class
69             provides the linkage between the two ends of the relationship ( the linking class
70             maps to the join table in the database).
71              
72             =back
73              
74             =head2 Plugin Methods
75              
76             =head3 build( $dbic_result, [$no_rel] )
77              
78             Takes a DBIx::Class result object and, if it's class matches your class's dbic()
79             method, attempts to build a new instance of your class based on the $dbic_result
80             passed in.
81              
82             The `no_rel` boolean option prevents the creation process from traversing
83             attributes marked as relationships, minimizing the amount of data pulled
84             from the database and the number of new class instances created.
85              
86             Returns the class instance if successful.
87              
88             =head3 save()
89              
90             Performs either an insert or an update for the model, depending on whether
91             there is already a record for it in the database. This method will propogate
92             changes from your DBIx::Class record back to your model, to account for DBIC
93             plugins you may be using that fiddle with column values on insert or update.
94              
95             Returns the calling object for convenient chaining.
96              
97             =head3 delete()
98              
99             Deletes the persistent copy of the current model from the database, if has
100             been stored there.
101              
102             Returns nothing.
103              
104             =head3 in_storage()
105              
106             Uses DBIx::Class's internal mechanisms to determine if this model
107             is tied to a record in the database.
108              
109             Returns a true value if it is, otherwise returns a false value.
110              
111             =cut
112              
113             class_has 'schema' => (
114             is => 'rw',
115             isa => 'DBIx::Class::Schema',
116             );
117              
118             # The actual ResultClass for the model object is stored here:
119             has '_dbic_result',
120             is => 'rw',
121             isa => 'Maybe[Object]',
122             lazy => 1,
123             default => sub {
124             my ( $self ) = @_;
125              
126             return $self->schema->resultset( $self->model->dbic )->new({});
127             };
128              
129              
130             sub configure {
131 8     8 1 55 my ( $plugin_class, $envoy_class, $conf ) = @_;
132              
133             $plugin_class->schema(
134             ref $conf->{schema} eq 'CODE' ? $conf->{schema}->($envoy_class) : $conf->{schema}
135 8 50       86 );
136              
137 8         38 $conf->{_configured} = 1;
138             }
139              
140              
141             sub build {
142 28     28 1 91 my ( $class, $model_class, $db_result, $no_rel ) = @_;
143              
144             return undef
145 28 50 33     273 unless $db_result
      33        
146             && blessed $db_result
147             && $db_result->isa( $model_class->dbic );
148              
149 28         261 my $data = $class->_data_for_model( $model_class, $db_result, $no_rel );
150 28         183 my $model = $model_class->new( %$data );
151              
152 28         30692 $model->get_storage( __PACKAGE__ )->_dbic_result( $db_result );
153              
154 28         159 return $model;
155             }
156              
157             sub fetch {
158 9     9 1 26 my $self = shift;
159 9         17 my $model_class = shift;
160 9         17 my %params;
161              
162 9 50       30 return undef unless @_;
163              
164 9 100       28 if ( @_ == 1 ) {
165              
166 3         8 my ( $id ) = @_;
167              
168 3         9 $params{id} = $id;
169             }
170             else {
171              
172 6         16 my ( $key, $value ) = @_;
173              
174 6         19 $params{$key} = $value;
175             }
176              
177 9 100       305 if ( my $result = ($self->schema->resultset( $model_class->dbic )
178             ->search(\%params))[0] ) {
179              
180 5         19110 return $model_class->build($result);
181             }
182              
183 3         7716 return undef;
184             }
185              
186             sub list {
187 7     7 1 17 my $self = shift;
188 7         11 my $model_class = shift;
189              
190 7 50       30 my $conditions = ref $_[0]
191             ? $_[0]
192             : { @_ };
193              
194             return [
195 7         243 map { $model_class->build($_) }
  4         12385  
196             $self->schema->resultset( $model_class->dbic )->search( $conditions )
197             ];
198             }
199              
200             sub save {
201 9     9 1 13300 my ( $self ) = @_;
202              
203 9         314 my $dbic_result = $self->_dbic_result;
204              
205             $self->schema->txn_do( sub {
206              
207             # First update/insert non-relationships
208 9     9   9257 $self->_populate_dbic_result;
209              
210 9 50       2159 if ( $dbic_result->in_storage ) {
    100          
211 0         0 $dbic_result->update;
212             }
213             # get_from_storage can be noisy due to
214             # https://rt.cpan.org/Public/Bug/Display.html?id=104839
215             elsif ( my $copy = $dbic_result->get_from_storage ) {
216 1         3101 $dbic_result->in_storage(1);
217 1         5 $dbic_result->update();
218             }
219             else {
220 8         36623 $dbic_result->insert;
221             }
222              
223             # Then, once we're sure the record exists, update relationships
224 9         21441 for my $attr ( @{$self->_dbic_relationships} ) {
  9         39  
225              
226 5         116 $self->_db_save_relationship( $attr );
227             }
228              
229 9         489 $dbic_result->update;
230              
231             # Finally, propogate any storage-layer changes back to model
232 9         753 $self->update_model( $dbic_result );
233              
234 9         289 });
235              
236 9         3915 return $self;
237             }
238              
239             sub _data_for_model {
240 41     41   110 my ( $class, $model_class, $db_result, $no_rel ) = @_;
241              
242 41         87 my $data = {};
243 41         83 my %relationships = map { $_->name => 1 } @{$class->_dbic_relationships($model_class)};
  35         2570  
  41         126  
244              
245 41         116 foreach my $attr ( grep { defined $db_result->$_ } map { $_->name } @{$class->_dbic_attrs($model_class)} ) {
  117         53031  
  117         372  
  41         152  
246              
247 114 100 100     36865 next if $no_rel && exists $relationships{$attr};
248              
249 101 100 100     1935 if ( blessed $db_result->$attr && $db_result->$attr->isa('DBIx::Class::ResultSet') ) {
250 17         8193 my $attribute = $model_class->meta->find_attribute_by_name($attr);
251 17         1214 my $class_attr = $attribute->meta->find_attribute_by_name('moose_class');
252 17 50       940 my $factory = $class_attr ? $class_attr->get_value($attribute) : undef;
253              
254 17   33     2425 $factory ||= ( $attribute->type_constraint->name =~ / (?:ArrayRef|Maybe) \[ (.+?) \] /x )[0];
255              
256 17 50       2524 if ( $factory ) {
257              
258 17         357 $data->{$attr} = [ map { $factory->build( $_, 1 ) } $db_result->$attr ];
  13         34796  
259             }
260             }
261             else {
262 84         2812 $data->{$attr} = $db_result->$attr;
263             }
264              
265             }
266              
267 41         16995 return $data;
268             }
269              
270             sub _db_save_relationship {
271 5     5   17 my ( $self, $attr ) = @_;
272              
273 5         159 my $dbic_result = $self->_dbic_result;
274              
275 5         27 my $name = $attr->name;
276 5         60 my $type = $attr->meta->get_attribute('rel')->get_value($attr);
277 5         2705 my $value = $self->model->$name;
278              
279 5 50       70 if ( $type eq 'many_to_many' ) {
    100          
280 0         0 my $setter = 'set_' . $name;
281 0         0 my $records = $self->_value_to_db( $value );
282 0         0 $dbic_result->$setter( $self->_value_to_db( $value ) );
283              
284 0         0 for ( my $i=0; $i < @$value; $i++ ) {
285 0         0 $value->[$i]->get_storage('DBIC')->update_model($records->[$i]);
286             }
287             }
288             elsif ( $type eq 'has_many' ) {
289              
290 4         18 foreach my $model ( @$value ) {
291 4         49 my $result = $self->_value_to_db( $model );
292              
293             # update_or_create_related can be noisy due to
294             # https://rt.cpan.org/Public/Bug/Display.html?id=104839
295 4         44 my $data = { $result->get_columns };
296             $result = $dbic_result->update_or_create_related( $name => {
297 7         61 map { $_ => $data->{$_} }
298 4         76 grep { defined $data->{$_} }
  12         32  
299             keys %$data
300             } );
301              
302 4         23297 $model->get_storage('DBIC')->update_model($result);
303             }
304             }
305             }
306              
307             sub update_model {
308 13     13 0 45 my ( $self, $dbic_result ) = @_;
309              
310 13         430 $self->model->update( $self->_data_for_model( ref $self->model, $dbic_result ) );
311             }
312              
313             sub delete {
314 3     3 1 5611 my ( $self ) = @_;
315              
316 3 50       105 if ( $self->_dbic_result->in_storage ) {
317 3         87 $self->_dbic_result->delete;
318             }
319              
320 3         18809 return;
321             }
322              
323             sub in_storage {
324 10     10 1 25 my ( $self ) = @_;
325              
326 10         301 return $self->_dbic_result->in_storage;
327             }
328              
329             class_has '_cached_dbic_attrs' => (
330             is => 'rw',
331             isa => 'HashRef',
332             default => sub { {} },
333             );
334              
335             sub _dbic_attrs {
336 41     41   99 my ( $self, $model ) = @_;
337              
338 41   33     116 $model //= $self->model;
339              
340 41         91 my $model_class = ref $model;
341 41   33     229 $model_class ||= $model;
342              
343 41 100       1376 if ( ! $self->_cached_dbic_attrs->{ $model_class } ) {
344              
345             $self->_cached_dbic_attrs->{ $model_class } = [
346 11         53 grep { $_->does('DBIC') }
  49         10301  
347             $model->meta->get_all_attributes
348             ];
349             }
350              
351 41         1258 return $self->_cached_dbic_attrs->{ $model_class }
352              
353             }
354              
355             sub _dbic_columns {
356 14     14   38 my ( $self, $model ) = @_;
357              
358 14   33     485 $model //= $self->model;
359              
360             return [
361 14 100       98 grep { $_->does('DBIC') && ! $_->is_relationship }
  59         4202  
362             $model->meta->get_all_attributes
363             ];
364             }
365              
366             sub _dbic_relationships {
367 64     64   151 my ( $self, $model ) = @_;
368              
369 64   66     890 $model //= $self->model;
370              
371             return [
372 64 100       305 grep { $_->does('DBIC') && $_->is_relationship }
  291         19410  
373             $model->meta->get_all_attributes
374             ];
375             }
376              
377             sub _dbic_fk_relationships {
378 14     14   42 my ( $self, $model ) = @_;
379              
380             return [
381             grep {
382 9         216 my $type = $_->meta->get_attribute('rel')->get_value($_);
383 9 100       1618 $type eq 'belongs_to' ? 1 : 0;
384             }
385 14         33 @{$self->_dbic_relationships($model)}
  14         49  
386             ];
387             }
388              
389             sub _populate_dbic_result {
390 14     14   49 my ( $self ) = @_;
391              
392 14         28 for my $attr ( @{$self->_dbic_columns}, @{$self->_dbic_fk_relationships} ) {
  14         49  
  14         325  
393              
394 33         4718 my $name = $attr->name;
395 33         1047 my $value = $self->_value_to_db( $self->model->$name );
396              
397 33         995 $self->_dbic_result->$name( $value );
398             }
399             }
400              
401             sub _value_to_db {
402 37     37   364 my ( $self, $value ) = @_;
403              
404 37 50 66     322 if ( ref $value eq 'ARRAY' ) {
    100 66        
405              
406 0         0 return [ map { $self->_value_to_db($_) } @$value ];
  0         0  
407             }
408             elsif ( blessed $value && $value->can('does') && $value->does('Model::Envoy') ) {
409              
410 5         1836 my $dbic = $value->get_storage(ref $self);
411              
412 5         31 $dbic->_populate_dbic_result;
413 5         5224 return $dbic->_dbic_result;
414             }
415              
416 32         81 return $value;
417             }
418              
419             package MooseX::Meta::Attribute::Trait::DBIC;
420 10     10   3139441 use Moose::Role;
  10         28  
  10         68  
421             Moose::Util::meta_attribute_alias('DBIC');
422              
423 10     10   55056 use Moose::Util::TypeConstraints 'enum';
  10         30  
  10         94  
424              
425             has rel => (
426             is => 'ro',
427             isa => enum(['belongs_to','has_many','many_to_many']),
428             predicate => 'is_relationship'
429             );
430              
431             has mm_rel => (
432             is => 'ro',
433             isa => 'Str',
434             predicate => 'is_many_to_many',
435             );
436              
437             has primary_key => (
438             is => 'ro',
439             isa => 'Bool',
440             predicate => 'is_primary_key'
441             );
442              
443             package Moose::Meta::Attribute::Custom::Trait::EDBIC;
444             sub register_implementation {
445 0     0     'MooseX::Meta::Attribute::Trait::DBIC'
446             };
447              
448             1;