root/trunk/lib/Data/ObjectDriver/BaseObject.pm @ 208

Revision 208, 14.9 kB (checked in by btrott, 4 years ago)

Merged in the relevant changes from the archetype-0.4 branch. All tests
still pass, which is good.

svn merge -r163:207
http://code.sixapart.com/svn/Data-ObjectDriver/branches/archetype-0.4

  • Property svn:keywords set to Id Revision
Line 
1# $Id$
2
3package Data::ObjectDriver::BaseObject;
4use strict;
5use warnings;
6
7use Scalar::Util qw(weaken);
8use Carp ();
9
10use Class::Trigger qw( pre_save post_save post_load pre_search
11                       pre_insert post_insert pre_update post_update
12                       pre_remove post_remove );
13
14sub install_properties {
15    my $class = shift;
16    no strict 'refs';
17    my($props) = @_;
18    *{"${class}::__properties"} = sub { $props };
19
20    # predefine getter/setter methods here
21    foreach my $col (@{ $props->{columns} }) {
22        # Skip adding this method if the class overloads it.
23        # this lets the SUPER::columnname magic do it's thing
24        if (!defined (*{"${class}::$col"})) {
25            *{"${class}::$col"} = $class->column_func($col);
26        }
27    }
28    $props;
29}
30
31sub properties {
32    my $this = shift;
33    my $class = ref($this) || $this;
34    $class->__properties;
35}
36
37# see docs below
38
39sub has_a {
40    my $class = shift;
41    my @args = @_;
42
43    # Iterate over each remote object
44    foreach my $config (@args) {
45        my $parentclass = $config->{class};
46
47        # Parameters
48        my $column = $config->{column};
49        my $method = $config->{method};
50        my $cached = $config->{cached} || 0;
51        my $parent_method = $config->{parent_method};
52
53        # column is required
54        if (!defined($column)) {
55            die "Please specify a valid column for $parentclass" 
56        }
57
58        # create a method name based on the column
59        if (! defined $method) {
60            if (!ref($column)) {
61                $method = $column;
62                $method =~ s/_id$//;
63                $method .= "_obj";
64            } elsif (ref($column) eq 'ARRAY') {
65                foreach my $col (@{$column}) {
66                    $col =~ s/_id$//;
67                    $method .= $col . '_';
68                }
69                $method .= "obj";
70            }
71        }
72
73        # die if we have clashing methods method
74        if (! defined $method || defined(*{"${class}::$method"})) {
75            die "Please define a valid method for $class->$column";
76        }
77
78        if ($cached) {
79            # Store cached item inside this object's namespace
80            my $cachekey = "__cache_$method";
81
82            no strict 'refs';
83            *{"${class}::$method"} = sub {
84                my $obj = shift;
85
86                return $obj->{$cachekey}
87                    if defined $obj->{$cachekey};
88
89                my $id = (ref($column) eq 'ARRAY')
90                    ? [ map { $obj->{column_values}->{$_} } @{$column}]
91                    : $obj->{column_values}->{$column}
92                    ;
93                ## Hold in a variable here too, so we don't lose it immediately
94                ## by having only the weak reference.
95                my $ret = $obj->{$cachekey} = $parentclass->lookup($id);
96                weaken $obj->{$cachekey};
97                return $ret;
98            };
99        } else {
100            if (ref($column)) {
101                no strict 'refs';
102                *{"${class}::$method"} = sub {
103                    my $obj = shift;
104                    return $parentclass->lookup([ map{ $obj->{column_values}->{$_} } @{$column}]);
105                };
106            } else {
107                no strict 'refs';
108                *{"${class}::$method"} = sub {
109                    return $parentclass->lookup(shift()->{column_values}->{$column});
110                };
111            }
112        }
113
114        # now add to the parent
115        if (!defined $parent_method) {
116            $parent_method = lc($class);
117            $parent_method =~ s/^.*:://; 
118
119            $parent_method .= '_objs';
120        }
121        if (ref($column)) {
122            no strict 'refs';
123            *{"${parentclass}::$parent_method"} = sub {
124                my $obj = shift;
125                my $terms = shift || {};
126                my $args = shift;
127
128                my $primary_key_tuple = $obj->primary_key_tuple;
129                my $primary_key = $obj->primary_key;
130
131                # inject pk search into given terms.
132                # composite key, ugh
133                foreach my $key (@{$primary_key_tuple}) {
134                    $terms->{$key} = shift(@{$primary_key});
135                }
136
137                return $class->search($terms, $args);
138            };
139        } else {
140            no strict 'refs';
141            *{"${parentclass}::$parent_method"} = sub {
142                my $obj = shift;
143                my $terms = shift || {};
144                my $args = shift;
145                # TBD - use primary_key_to_terms
146                $terms->{$column} = $obj->primary_key;
147                return $class->search($terms, $args);
148            };
149        };
150    } # end of loop over class names
151    return;
152}
153
154sub driver {
155    my $class = shift;
156    $class->properties->{driver} ||= $class->properties->{get_driver}->();
157}
158
159sub get_driver {
160    my $class = shift;
161    $class->properties->{get_driver} = shift if @_;
162}
163
164sub new { bless {}, shift }
165
166sub is_pkless {
167    my $obj = shift;
168    my $prop_pk = $obj->properties->{primary_key};
169    return 1 if ! $prop_pk;
170    return 1 if ref $prop_pk eq 'ARRAY' && ! @$prop_pk;
171}
172
173sub is_primary_key {
174    my $obj = shift;
175    my($col) = @_;
176
177    my $prop_pk = $obj->properties->{primary_key};
178    if (ref($prop_pk)) {
179        for my $pk (@$prop_pk) {
180            return 1 if $pk eq $col;
181        }
182    } else {
183        return 1 if $prop_pk eq $col;
184    }
185
186    return;
187}
188
189sub primary_key_tuple {
190    my $obj = shift;
191    my $pk = $obj->properties->{primary_key};
192    $pk = [ $pk ] unless ref($pk) eq 'ARRAY';
193    $pk;
194}
195
196sub primary_key {
197    my $obj = shift;
198    my $pk = $obj->primary_key_tuple;
199    my @val = map { $obj->$_() }  @$pk;
200    @val == 1 ? $val[0] : \@val;
201}
202
203sub is_same_array {
204    my($a1, $a2) = @_;
205    return if ($#$a1 != $#$a2);
206    for (my $i = 0; $i <= $#$a1; $i++) {
207        return if $a1->[$i] ne $a2->[$i];
208    }
209    return 1;
210}
211
212sub primary_key_to_terms {
213    my($obj, $id) = @_;
214    my $pk = $obj->primary_key_tuple;
215    if (! defined $id) { 
216        $id = $obj->primary_key;
217    } else {
218        if (ref($id) eq 'HASH') {
219            my @keys = sort keys %$id;
220            unless (is_same_array(\@keys, [ sort @$pk ])) {
221                Carp::croak("keys don't match with primary keys: @keys");
222            }
223            return $id;
224        }
225    }
226    $id = [ $id ] unless ref($id) eq 'ARRAY';
227    my $i = 0;
228    my %terms;
229    @terms{@$pk} = @$id;
230    \%terms;
231}
232
233sub has_primary_key {
234    my $obj = shift;
235    return unless @{$obj->primary_key_tuple};
236    my $val = $obj->primary_key;
237    $val = [ $val ] unless ref($val) eq 'ARRAY';
238    for my $v (@$val) {
239        return unless defined $v;
240    }
241    1;
242}
243
244sub datasource { $_[0]->properties->{datasource} }
245
246sub columns_of_type {
247    my $obj = shift;
248    my($type) = @_;
249    my $props = $obj->properties;
250    my $cols = $props->{columns};
251    my $col_defs = $props->{column_defs};
252    my @cols;
253    for my $col (@$cols) {
254        push @cols, $col if $col_defs->{$col} && $col_defs->{$col} eq $type;
255    }
256    \@cols;
257}
258
259sub set_values {
260    my $obj = shift;
261    my $values = shift;
262    for my $col (keys %$values) {
263        unless ( $obj->has_column($col) ) {
264            Carp::croak("You tried to set inexistent column $col to value $values->{$col} on " . ref($obj));
265        }
266        $obj->$col($values->{$col});
267    }
268}
269
270sub set_values_internal {
271    my $obj = shift;
272    my $values = shift;
273    for my $col (keys %$values) {
274        # Not needed for the internal version of this method
275        #unless ( $obj->has_column($col) ) {
276        #    Carp::croak("You tried to set inexistent column $col to value $values->{$col} on " . ref($obj));
277        #}
278
279        $obj->column_values->{$col} = $values->{$col};
280    }
281}
282
283sub clone {
284    my $obj = shift;
285    my $clone = $obj->clone_all;
286    for my $pk (@{ $obj->primary_key_tuple }) {
287        $clone->$pk(undef);
288    }
289    $clone;
290}
291
292sub clone_all {
293    my $obj = shift;
294    my $clone = ref($obj)->new();
295    $clone->set_values_internal($obj->column_values);
296    $clone->{changed_cols} = $obj->{changed_cols};
297    $clone;
298}
299
300sub has_column {
301    my $obj = shift;
302    my($col) = @_;
303    $obj->{__col_names} ||= { map { $_ => 1 } @{ $obj->column_names } };
304    exists $obj->{__col_names}->{$col};
305}
306
307sub column_names {
308    ## Reference to a copy.
309    [ @{ shift->properties->{columns} } ]
310}
311
312sub column_values { $_[0]->{'column_values'} ||= {} }
313
314## In 0.1 version we didn't die on inexistent column
315## which might lead to silent bugs
316## You should override column if you want to find the old
317## behaviour
318sub column {
319    my $obj = shift;
320    my $col = shift or return;
321    unless ($obj->has_column($col)) {
322        Carp::croak("Cannot find column '$col' for class '" . ref($obj) . "'");
323    }
324
325    # set some values
326    if (@_) {
327        $obj->{column_values}->{$col} = shift;
328        unless ($_[0] && ref($_[0]) eq 'HASH' && $_[0]->{no_changed_flag}) {
329            $obj->{changed_cols}->{$col}++;
330        }
331    }
332
333    $obj->{column_values}->{$col};
334}
335
336sub column_func {
337    my $obj = shift;
338    my $col = shift or die "Must specify column";
339
340    return sub {
341        my $obj = shift;
342        # getter
343        return $obj->{column_values}->{$col} unless (@_);
344
345        # setter
346        my ($val, $flags) = @_;
347        $obj->{column_values}->{$col} = $val;
348        unless (($val && ref($val) eq 'HASH' && $val->{no_changed_flag}) ||
349                $flags->{no_changed_flag}) {
350            $obj->{changed_cols}->{$col}++;
351        }
352
353        return $obj->{column_values}->{$col};
354    };
355}
356
357
358sub changed_cols_and_pk {
359    my $obj = shift;
360    keys %{$obj->{changed_cols}};
361}
362
363sub changed_cols {
364    my $obj = shift;
365    my $pk = $obj->primary_key_tuple;
366    my %pk = map { $_ => 1 } @$pk;
367    grep !$pk{$_}, $obj->changed_cols_and_pk;
368}
369
370sub is_changed {
371    my $obj = shift;
372    if (@_) {
373        return exists $obj->{changed_cols}->{$_[0]};
374    } else {
375        return $obj->changed_cols > 0;
376    }
377}
378
379sub exists {
380    my $obj = shift;
381    return 0 unless $obj->has_primary_key;
382    $obj->_proxy('exists', @_);
383}
384
385sub save {
386    my $obj = shift;
387    if ($obj->exists) {
388        return $obj->update;
389    } else {
390        return $obj->insert;
391    }
392}
393
394sub lookup {
395    my $class = shift;
396    my $driver = $class->driver;
397    my $obj = $driver->lookup($class, @_) or return;
398    $driver->cache_object($obj);
399    $obj;
400}
401
402sub lookup_multi {
403    my $class = shift;
404    my $driver = $class->driver;
405    my $objs = $driver->lookup_multi($class, @_) or return;
406    for my $obj (@$objs) {
407        $driver->cache_object($obj) if $obj;
408    }
409    $objs;
410}
411
412sub search {
413    my $class = shift;
414    my($terms, $args) = @_;
415    my $driver = $class->driver;
416    my @objs = $driver->search($class, $terms, $args);
417
418    ## Don't attempt to cache objects where the caller specified fetchonly,
419    ## because they won't be complete.
420    ## Also skip this step if we don't get any objects back from the search
421    if (!$args->{fetchonly} || !@objs) {
422        for my $obj (@objs) {
423            $driver->cache_object($obj) if $obj;
424        }
425    }
426    $driver->list_or_iterator(\@objs);
427}
428
429sub remove          { shift->_proxy('remove',       @_) }
430sub update          { shift->_proxy('update',       @_) }
431sub insert          { shift->_proxy('insert',       @_) }
432sub fetch_data      { shift->_proxy('fetch_data',   @_) }
433
434sub refresh {
435    my $obj = shift; 
436    return unless $obj->has_primary_key;
437    my $fields = $obj->fetch_data;
438    $obj->set_values_internal($fields);
439    # XXX not sure this is the right place
440    $obj->call_trigger('post_load');
441    return 1;
442}
443
444sub _proxy {
445    my $obj = shift;
446    my($meth, @args) = @_;
447    $obj->driver->$meth($obj, @args);
448}
449
450sub deflate { { columns => shift->column_values } }
451
452sub inflate {
453    my $class = shift;
454    my($deflated) = @_;
455    my $obj = $class->new;
456    $obj->set_values_internal($deflated->{columns});
457    $obj->{changed_cols} = {};
458    $obj;
459}
460
461sub DESTROY { }
462
463sub AUTOLOAD {
464    my $obj = $_[0];
465    (my $col = our $AUTOLOAD) =~ s!.+::!!;
466    no strict 'refs';
467    Carp::croak("Cannot find method '$col' for class '$obj'") unless ref $obj;
468    unless ($obj->has_column($col)) {
469        Carp::croak("Cannot find column '$col' for class '" . ref($obj) . "'");
470    }
471
472    *$AUTOLOAD = $obj->column_func($col);
473
474    goto &$AUTOLOAD;
475}
476
4771;
478__END__
479
480=head1 NAME
481
482Data::ObjectDriver::BaseObject - base class for modeled objects
483
484=head1 SYNOPSIS
485
486See synopsis in I<Data::ObjectDriver>.
487
488=head1 DESCRIPTION
489
490I<Data::ObjectDriver::BaseObject> provides services to data objects modeled
491with the I<Data::ObjectDriver> object relational mapper.
492
493=head1 USAGE
494
495=head2 Class->install_properties({ ... })
496
497Sets up columns, indexes, primary keys, etc.
498
499=head2 Class->properties
500
501Returns the list of properties.
502
503=head2 Class->has_a(ParentClass => { ... }, ParentClass2 => { ...} )
504
505Creates utility methods that map this object to parent Data::ObjectDriver objects.
506
507Pass in a list of parent classes to map with a hash of parameters.  The following parameters
508are recognized:
509
510=over 4
511
512=item * column
513
514Name of the column(s) in this class to map with.  Pass in a single string if
515the column is a singular key, an array ref if this is a composite key.
516
517   column => 'user_id'
518   column => ['user_id', 'photo_id']
519
520=item * method [OPTIONAL]
521
522Name of the method to create in this class.  Defaults to the column name(s) without
523the _id suffix and with the suffix _obj appended.
524
525=item * parent_method [OPTIONAL]
526
527Name of the method created in the parent class.  Default is the lowercased
528name of the current class with the suffix _objs.
529
530=item * cached [OPTIONAL]
531
532If set to 1 cache the result of the fetching the parent object in the current class.  Note
533that this is a private copy to this class only, and does not interact with other caches
534in the system.
535
536=back
537
538=head2 column_func
539
540This method is called to get/set column values.  Subclasses can override this and get different
541behavior.
542
543=head2 Class->driver
544
545Returns the database driver for this class, invoking the class's I<get_driver>
546function if necessary.
547
548=head2 Class->get_driver($driver)
549
550Sets the function used to find the object driver for I<Class> objects.
551
552=head2 $obj->primary_key
553
554Returns the B<values> of the primary key fields of I<$obj>.
555
556=head2 Class->primary_key_tuple
557
558Returns the B<names> of the primary key fields for objects of class I<Class>.
559
560=head2 $obj->has_primary_key
561
562=head2 $obj->clone
563
564Returns a new object of the same class as I<$obj> containing the same data,
565except for primary keys, which are set to C<undef>.
566
567=head2 $obj->clone_all
568
569Returns a new object of the same class as I<$obj> containing the same data,
570including all key fields.
571
572=head2 $obj->deflate
573
574Returns a minimal representation of the object, for use in caches where
575you might want to preserve space (like memcached). Can also be overridden
576by subclasses to store the optimal representation of an object in the
577cache. For example, if you have metadata attached to an object, you might
578want to store that in the cache, as well.
579
580=head2 $class->inflate($deflated)
581
582Inflates the deflated representation of the object I<$deflated> into a
583proper object in the class I<$class>.
584
585=cut
Note: See TracBrowser for help on using the browser.