File Coverage

blib/lib/Rose/DB/Object/Cached.pm
Criterion Covered Total %
statement 70 211 33.1
branch 8 52 15.3
condition 2 26 7.6
subroutine 21 31 67.7
pod 10 10 100.0
total 111 330 33.6


line stmt bran cond sub pod time code
1             package Rose::DB::Object::Cached;
2              
3 2     2   940539 use strict;
  2         12  
  2         61  
4              
5 2     2   10 use Carp();
  2         4  
  2         39  
6              
7 2     2   745 use Rose::DB::Object;
  2         8  
  2         96  
8             our @ISA = qw(Rose::DB::Object);
9              
10 2     2   13 use Rose::DB::Object::Constants qw(STATE_IN_DB);
  2         5  
  2         162  
11              
12             our $VERSION = '0.785';
13              
14             our $Debug = 0;
15              
16             # Anything that cannot be in a column name will work for these
17 2     2   15 use constant PK_SEP => "\0\0";
  2         82  
  2         157  
18 2     2   14 use constant UK_SEP => "\0\0";
  2         4  
  2         120  
19              
20             # Try to pick a very unlikely value to stand in for undef in
21             # the stringified multi-column unique key value
22 2     2   14 use constant UNDEF => "\1\2undef\2\1";
  2         4  
  2         251  
23              
24             sub remember
25             {
26 0     0 1 0 my($self) = shift;
27              
28 0         0 my $class = ref $self;
29 0         0 my $meta = $self->meta;
30 0         0 my $pk = join(PK_SEP, grep { defined } map { $self->$_() } $meta->primary_key_column_accessor_names);
  0         0  
  0         0  
31              
32 2     2   14 no strict 'refs';
  2         5  
  2         246  
33              
34 0   0     0 my $ttl_secs = $class->meta->cached_objects_expire_in || 0;
35 0 0       0 my $loaded = $ttl_secs ? time : 0;
36              
37 0         0 ${"${class}::Objects_By_Id"}{$pk} = $self;
  0         0  
38              
39 0 0       0 if($ttl_secs)
40             {
41 0         0 ${"${class}::Objects_By_Id_Loaded"}{$pk} = $loaded;
  0         0  
42             }
43              
44 0         0 my $accessor = $meta->column_accessor_method_names_hash;
45              
46 0         0 foreach my $cols ($self->meta->unique_keys_column_names)
47             {
48 2     2   14 no warnings;
  2         4  
  2         436  
49 0         0 my $key_name = join(UK_SEP, @$cols);
50 0 0       0 my $key_value = join(UK_SEP, grep { defined($_) ? $_ : UNDEF }
51 0         0 map { my $m = $accessor->{$_}; $self->$m() } @$cols);
  0         0  
  0         0  
52              
53 0         0 ${"${class}::Objects_By_Key"}{$key_name}{$key_value} = $self;
  0         0  
54 0         0 ${"${class}::Objects_Keys"}{$pk}{$key_name} = $key_value;
  0         0  
55              
56 0 0       0 if($ttl_secs)
57             {
58 0         0 ${"${class}::Objects_By_Key_Loaded"}{$key_name}{$key_value} = $loaded;
  0         0  
59             }
60             }
61             };
62              
63             # This constant is not arbitrary. It must be defined and false.
64             # I'm playing games with return values, but this is all internal
65             # anyway and can change at any time.
66 2     2   15 use constant CACHE_EXPIRED => 0;
  2         6  
  2         249  
67              
68             sub __xrdbopriv_get_object
69             {
70 0   0 0   0 my($class) = ref $_[0] || $_[0];
71              
72 0         0 my $ttl_secs = $class->meta->cached_objects_expire_in;
73              
74 0 0       0 if(@_ == 2)
75             {
76 0         0 my($pk) = $_[1];
77              
78 2     2   15 no strict 'refs';
  2         3  
  2         102  
79 2     2   16 no warnings;
  2         4  
  2         300  
80              
81 0 0       0 if(${"${class}::Objects_By_Id"}{$pk})
  0         0  
82             {
83 0 0 0     0 if($ttl_secs && (time - ${"${class}::Objects_By_Id_Loaded"}{$pk}) >= $ttl_secs)
  0         0  
84             {
85 0         0 delete ${"${class}::Objects_By_Id"}{$pk};
  0         0  
86 0         0 return CACHE_EXPIRED;
87             }
88              
89 0         0 return ${"${class}::Objects_By_Id"}{$pk};
  0         0  
90             }
91              
92 0         0 return undef;
93             }
94             else
95             {
96 0         0 my($key_name, $key_value) = ($_[1], $_[2]);
97              
98 2     2   15 no strict 'refs';
  2         4  
  2         69  
99 2     2   13 no warnings;
  2         4  
  2         668  
100              
101 0 0       0 if(${"${class}::Objects_By_Key"}{$key_name}{$key_value})
  0         0  
102             {
103 0 0 0     0 if($ttl_secs && (time - ${"${class}::Objects_By_Key_Loaded"}{$key_name}{$key_value}) >= $ttl_secs)
  0         0  
104             {
105 0         0 delete ${"${class}::Objects_By_Key_Loaded"}{$key_name}{$key_value};
  0         0  
106 0         0 return undef; # cache expired
107             }
108              
109 0         0 ${"${class}::Objects_By_Key"}{$key_name}{$key_value}->remember();
  0         0  
110 0         0 return ${"${class}::Objects_By_Key"}{$key_name}{$key_value};
  0         0  
111             }
112              
113 0         0 return undef;
114             }
115             };
116              
117             sub load
118             {
119             # XXX: Must maintain alias to actual "self" object arg
120              
121 0     0 1 0 my %args = (self => @_); # faster than @_[1 .. $#_];
122              
123 0 0       0 unless(delete $args{'refresh'})
124             {
125 0         0 my $pk = join(PK_SEP, grep { defined } map { $_[0]->$_() } $_[0]->meta->primary_key_column_accessor_names);
  0         0  
  0         0  
126              
127 0         0 my $object = __xrdbopriv_get_object($_[0], $pk);
128              
129 0 0 0     0 if($object)
    0          
130             {
131 0         0 $_[0] = $object;
132 0         0 $_[0]->{STATE_IN_DB()} = 1;
133 0   0     0 return $_[0] || 1;
134             }
135             elsif(!(defined $object && $object == CACHE_EXPIRED))
136             {
137 0         0 my $meta = $_[0]->meta;
138 0         0 my $accessor = $meta->column_accessor_method_names_hash;
139              
140 0         0 foreach my $cols ($meta->unique_keys_column_names)
141             {
142 2     2   15 no warnings;
  2         4  
  2         1010  
143 0         0 my $key_name = join(UK_SEP, @$cols);
144 0 0       0 my $key_value = join(UK_SEP, grep { defined($_) ? $_ : UNDEF }
145 0         0 map { my $m = $accessor->{$_}; $_[0]->$m() } @$cols);
  0         0  
  0         0  
146              
147 0 0       0 if(my $object = __xrdbopriv_get_object($_[0], $key_name, $key_value))
148             {
149 0         0 $_[0] = $object;
150 0         0 $_[0]->{STATE_IN_DB()} = 1;
151 0   0     0 return $_[0] || 1;
152             }
153             }
154             }
155             }
156              
157 0         0 my $ret = $_[0]->SUPER::load(%args);
158 0 0       0 $_[0]->remember if($ret);
159              
160 0         0 return $ret;
161             }
162              
163             sub insert
164             {
165 0     0 1 0 my($self) = shift;
166              
167 0         0 my $ret = $self->SUPER::insert(@_);
168 0 0       0 return $ret unless($ret);
169              
170 0         0 $self->remember;
171              
172 0         0 return $ret;
173             }
174              
175             sub update
176             {
177 0     0 1 0 my($self) = shift;
178              
179 0         0 my $ret = $self->SUPER::update(@_);
180 0 0       0 return $ret unless($ret);
181              
182 0         0 $self->remember;
183              
184 0         0 return $ret;
185             }
186              
187             sub delete
188             {
189 0     0 1 0 my($self) = shift;
190 0         0 my $ret = $self->SUPER::delete(@_);
191 0 0       0 $self->forget if($ret);
192 0         0 return $ret;
193             }
194              
195             sub forget
196             {
197 0     0 1 0 my($self) = shift;
198              
199 0         0 my $class = ref $self;
200 0         0 my $pk = join(PK_SEP, grep { defined } map { $self->$_() } $self->meta->primary_key_column_accessor_names);
  0         0  
  0         0  
201              
202 2     2   17 no strict 'refs';
  2         19  
  2         122  
203 0         0 delete ${"${class}::Objects_By_Id"}{$pk};
  0         0  
204              
205 0         0 foreach my $cols ($self->meta->unique_keys_column_names)
206             {
207 2     2   14 no warnings;
  2         4  
  2         428  
208 0         0 my $key_name = join(UK_SEP, @$cols);
209 0         0 my $key_value = ${"${class}::Objects_Keys"}{$pk}{$key_name};
  0         0  
210 0         0 delete ${"${class}::Objects_By_Key"}{$key_name}{$key_value};
  0         0  
211             }
212              
213 0         0 delete ${"${class}::Objects_Keys"}{$pk};
  0         0  
214              
215 0         0 return 1;
216             }
217              
218             sub remember_by_primary_key
219             {
220 0     0 1 0 my($self) = shift;
221              
222 0         0 my $class = ref $self;
223 0         0 my $pk = join(PK_SEP, grep { defined } map { $self->$_() } $self->meta->primary_key_column_accessor_names);
  0         0  
  0         0  
224              
225 2     2   16 no strict 'refs';
  2         4  
  2         683  
226 0         0 ${"${class}::Objects_By_Id"}{$pk} = $self;
  0         0  
227             }
228              
229             sub remember_all
230             {
231 0     0 1 0 my($class) = shift;
232              
233 0         0 require Rose::DB::Object::Manager;
234              
235 0         0 my(undef, %args) = Rose::DB::Object::Manager->normalize_get_objects_args(@_);
236              
237 0         0 my $objects =
238             Rose::DB::Object::Manager->get_objects(
239             object_class => $class,
240             share_db => 0,
241             %args);
242              
243 0         0 foreach my $object (@$objects)
244             {
245 0         0 $object->remember;
246             }
247              
248 0 0       0 return @$objects if(defined wantarray);
249             }
250              
251             # Code borrowed from Cache::Cache
252             my %Expiration_Units =
253             (
254             map(($_, 1), qw(s sec secs second seconds)),
255             map(($_, 60), qw(m min mins minute minutes)),
256             map(($_, 60*60), qw(h hr hrs hour hours)),
257             map(($_, 60*60*24), qw(d day days)),
258             map(($_, 60*60*24*7), qw(w wk wks week weeks)),
259             map(($_, 60*60*24*365), qw(y yr yrs year years))
260             );
261              
262             sub clear_object_cache
263             {
264 0   0 0 1 0 my($class) = ref($_[0]) || $_[0];
265              
266 2     2   19 no strict 'refs';
  2         12  
  2         346  
267 0         0 %{"${class}::Objects_By_Id"} = ();
  0         0  
268 0         0 %{"${class}::Objects_By_Key"} = ();
  0         0  
269 0         0 %{"${class}::Objects_Keys"} = ();
  0         0  
270              
271 0 0       0 if($class->cached_objects_expire_in)
272             {
273 0         0 %{"${class}::Objects_By_Key_Loaded"} = ();
  0         0  
274 0         0 %{"${class}::Objects_By_Id_Loaded"} = ();
  0         0  
275             }
276              
277 0         0 return 1;
278             }
279              
280             sub cached_objects_expire_in
281             {
282 196     196 1 742 my($class) = shift;
283              
284 196 100       380 $class = ref($class) if(ref($class));
285              
286 2     2   14 no strict 'refs';
  2         14  
  2         685  
287 196 100 50     388 return ${"${class}::Cache_Expires"} ||= 0 unless(@_);
  112         509  
288              
289 84         139 my $arg = shift;
290              
291 84         123 my $secs;
292              
293 84 50 33     1078 if($arg =~ /^now$/i)
    50          
    50          
    50          
294             {
295 0         0 $class->forget_all;
296 0         0 $secs = 0;
297             }
298             elsif($arg =~ /^never$/)
299             {
300 0         0 $secs = 0;
301             }
302             elsif($arg =~ /^\s*([+-]?(?:\d+|\d*\.\d*))\s*$/)
303             {
304 0         0 $secs = $arg;
305             }
306             elsif($arg =~ /^\s*([+-]?(?:\d+(?:\.\d*)?|\d*\.\d+))\s*(\w*)\s*$/ && exists $Expiration_Units{$2})
307             {
308 84         261 $secs = $Expiration_Units{$2} * $1;
309             }
310             else
311             {
312 0         0 Carp::croak("Invalid cache expiration time: '$arg'");
313             }
314              
315 84         160 return ${"${class}::Cache_Expires"} = $secs;
  84         355  
316             }
317              
318             1;
319              
320             __END__
321              
322             =head1 NAME
323              
324             Rose::DB::Object::Cached - Memory cached object representation of a single row in a database table.
325              
326             =head1 SYNOPSIS
327              
328             package Category;
329              
330             use base 'Rose::DB::Object::Cached';
331              
332             __PACKAGE__->meta->setup
333             (
334             table => 'categories',
335              
336             columns =>
337             [
338             id => { type => 'int', primary_key => 1 },
339             name => { type => 'varchar', length => 255 },
340             description => { type => 'text' },
341             ],
342              
343             unique_key => 'name',
344             );
345              
346             ...
347              
348             $cat1 = Category->new(id => 123,
349             name => 'Art');
350              
351             $cat1->save or die $category->error;
352              
353              
354             $cat2 = Category->new(id => 123);
355              
356             # This will load from the memory cache, not the database
357             $cat2->load or die $cat2->error;
358              
359             # $cat2 is the same object as $cat1
360             print "Yep, cached" if($cat1 eq $cat2);
361              
362             # No, really, it's the same object
363             $cat1->name('Blah');
364             print $cat2->name; # prints "Blah"
365              
366             # The object cache supports time-based expiration
367             Category->cached_objects_expire_in('15 minutes');
368              
369             $cat1 = Category->new(id => 123);
370             $cat1->save or $cat1->die;
371              
372             $cat1->load; # loaded from cache
373              
374             $cat2 = Category->new(id => 123);
375             $cat2->load; # loaded from cache
376              
377             <15 minutes pass>
378              
379             $cat3 = Category->new(id => 123);
380             $cat3->load; # NOT loaded from cache
381              
382             ...
383              
384             =head1 DESCRIPTION
385              
386             C<Rose::DB::Object::Cached> is a subclass of L<Rose::DB::Object> that is backed by a write-through memory cache. Whenever an object is loaded from or saved to the database, it is cached in memory. Any subsequent attempt to load an object of the same class with the same primary key or unique key value(s) will give you the cached object instead of loading from the database.
387              
388             This means that I<modifications to an object will also modify all other objects in memory that have the same primary key.> The L<synopsis|/SYNOPSIS> above highlights this fact.
389              
390             This class is most useful for encapsulating "read-only" rows, or other data that is updated very infrequently. In the C<Category> example above, it would be inefficient to repeatedly load category information in a long-running process (such as a mod_perl Apache web server) if that information changes infrequently.
391              
392             The memory cache can be cleared for an individual object or all objects of the same class. There is also support for simple time-based cache expiration. See the L<clear_object_cache|/clear_object_cache> and L<cached_objects_expire_in|/cached_objects_expire_in> methods for more information.
393              
394             Only the methods that are overridden or otherwise behaviorally modified are documented here. See the L<Rose::DB::Object> documentation for the rest.
395              
396             =head1 CLASS METHODS
397              
398             =over 4
399              
400             =item B<cached_objects_expire_in [DURATION]>
401              
402             This method controls the expiration of cached objects.
403              
404             If called with no arguments, the cache expiration limit in seconds is returned. If passed a DURATION, the cache expiration is set. Valid formats for DURATION are in the form "NUMBER UNIT" where NUMBER is a positive number and UNIT is one of the following:
405              
406             s sec secs second seconds
407             m min mins minute minutes
408             h hr hrs hour hours
409             d day days
410             w wk wks week weeks
411             y yr yrs year years
412              
413             All formats of the DURATION argument are converted to seconds. Days are exactly 24 hours, weeks are 7 days, and years are 365 days.
414              
415             If an object was read from the database the specified number of seconds ago or earlier, it is purged from the cache and reloaded from the database the next time it is loaded.
416              
417             A L<cached_objects_expire_in|/cached_objects_expire_in> value of undef or zero means that nothing will ever expire from the object cache. This is the default.
418              
419             =item B<clear_object_cache>
420              
421             Clear the memory cache for all objects of this class.
422              
423             =back
424              
425             =head1 OBJECT METHODS
426              
427             =over 4
428              
429             =item B<delete [PARAMS]>
430              
431             This method works like the L<delete|Rose::DB::Object/delete> method from L<Rose::DB::Object> except that it also calls the L<forget|/forget> method if the object was deleted successfully or did not exist in the first place.
432              
433             =item B<forget>
434              
435             Delete the current object from the memory cache.
436              
437             =item B<load [PARAMS]>
438              
439             Load an object based on either a primary key or a unique key.
440              
441             If the object exists in the memory cache, the current object "becomes" the cached object. See the L<synopsis|/SYNOPSIS> or L<description|/DESCRIPTION> above for more information.
442              
443             If the object is not in the memory cache, it is loaded from the database. If the load succeeds, it is also written to the memory cache.
444              
445             PARAMS are name/value pairs, and are optional. Valid parameters are:
446              
447             =over 4
448              
449             =item B<refresh>
450              
451             If set to a true value, then the data is always loaded from the database rather than from the memory cache. If the load succeeds, the object replaces whatever was in the cache. If it fails, the cache is not modified.
452              
453             =back
454              
455             Returns true if the object was loaded successfully, false if the row could not be loaded or did not exist in the database. The true value returned on success will be the object itself. If the object L<overload>s its boolean value such that it is not true, then a true value will be returned instead of the object itself.
456              
457             =item B<insert [PARAMS]>
458              
459             This method does the same thing as the L<Rose::DB::Object> L<method of the same name|Rose::DB::Object/insert>, except that it also saves the object to the memory cache if the insert succeeds. If it fails, the memory cache is not modified.
460              
461             =item B<remember>
462              
463             Save the current object to the memory cache I<without> saving it to the database as well. Objects are cached based on their primary key values and all their unique key values.
464              
465             =item B<remember_all [PARAMS]>
466              
467             Load and L<remember|/remember> all objects from this table, optionally filtered by PARAMS which can be any valid L<Rose::DB::Object::Manager-E<gt>get_objects()|Rose::DB::Object::Manager/get_objects> parameters. Remembered objects will replace any previously cached objects with the same keys.
468              
469             =item B<remember_by_primary_key [PARAMS]>
470              
471             Save the current object to the memory cache I<without> saving it to the database as well. The object will be cached based on its primary key value I<only>. This is unlike the L<remeber|/remember> method which caches objects based on their primary key values and all their unique key values.
472              
473             =item B<save [PARAMS]>
474              
475             This method does the same thing as the L<Rose::DB::Object> L<method of the same name|Rose::DB::Object/save>, except that it also saves the object to the memory cache if the save succeeds. If it fails, the memory cache is not modified.
476              
477             =item B<update [PARAMS]>
478              
479             This method does the same thing as the L<Rose::DB::Object> L<method of the same name|Rose::DB::Object/update>, except that it also saves the object to the memory cache if the update succeeds. If it fails, the memory cache is not modified.
480              
481             =back
482              
483             =head1 RESERVED METHODS
484              
485             In addition to the reserved methods listed in the L<Rose::DB::Object> documentation, the following method names are also reserved for objects that inherit from this class:
486              
487             cached_objects_expire_in
488             clear_object_cache
489             forget
490             remember
491             remember_all
492             remember_by_primary_key
493              
494             If you have a column with one of these names, you must alias it. See the L<Rose::DB::Object> documentation for more information on column aliasing and reserved methods.
495              
496             =head1 AUTHOR
497              
498             John C. Siracusa (siracusa@gmail.com)
499              
500             =head1 LICENSE
501              
502             Copyright (c) 2010 by John C. Siracusa. All rights reserved. This program is
503             free software; you can redistribute it and/or modify it under the same terms
504             as Perl itself.