File Coverage

blib/lib/Jifty/DBI/Record/Cachable.pm
Criterion Covered Total %
statement 3 3 100.0
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 4 4 100.0


line stmt bran cond sub pod time code
1             package Jifty::DBI::Record::Cachable;
2              
3 2     2   107672 use base qw(Jifty::DBI::Record);
  2         4  
  2         749  
4              
5             use Jifty::DBI::Handle;
6              
7             use Cache::Simple::TimedExpiry;
8              
9             use strict;
10             use warnings;
11              
12             =head1 NAME
13              
14             Jifty::DBI::Record::Cachable - records with caching behavior
15              
16             =head1 SYNOPSIS
17              
18             package Myrecord;
19             use base qw/Jifty::DBI::Record::Cachable/;
20              
21             =head1 DESCRIPTION
22              
23             This module subclasses the main L<Jifty::DBI::Record> package to add a
24             caching layer.
25              
26             The public interface remains the same, except that records which have
27             been loaded in the last few seconds may be reused by subsequent fetch
28             or load methods without retrieving them from the database.
29              
30             =head1 METHODS
31              
32             =cut
33              
34             my %_CACHES = ();
35              
36             sub _setup_cache {
37             my $self = shift;
38             my $cache = shift;
39             $_CACHES{$cache} = Cache::Simple::TimedExpiry->new();
40             $_CACHES{$cache}->expire_after( $self->_cache_config->{'cache_for_sec'} );
41             }
42              
43             =head2 flush_cache
44              
45             This class method flushes the _global_ Jifty::DBI::Record::Cachable
46             cache. All caches are immediately expired.
47              
48             =cut
49              
50             sub flush_cache {
51             %_CACHES = ();
52             }
53              
54             sub _key_cache {
55             my $self = shift;
56             my $cache = $self->_handle->dsn
57             . "-KEYS--"
58             . ( $self->{'_class'} ||= ref($self) );
59             $self->_setup_cache($cache) unless exists( $_CACHES{$cache} );
60             return ( $_CACHES{$cache} );
61              
62             }
63              
64             =head2 _flush_key_cache
65              
66             Blow away this record type's key cache
67              
68             =cut
69              
70             sub _flush_key_cache {
71             my $self = shift;
72             my $cache = $self->_handle->dsn
73             . "-KEYS--"
74             . ( $self->{'_class'} ||= ref($self) );
75             $self->_setup_cache($cache);
76             }
77              
78             sub _record_cache {
79             my $self = shift;
80             my $cache
81             = $self->_handle->dsn . "--" . ( $self->{'_class'} ||= ref($self) );
82             $self->_setup_cache($cache) unless exists( $_CACHES{$cache} );
83             return ( $_CACHES{$cache} );
84              
85             }
86              
87             sub load_from_hash {
88             my $self = shift;
89              
90             my ( $rvalue, $msg );
91             if ( ref($self) ) {
92              
93             # Blow away the primary cache key since we're loading.
94             $self->{'_jifty_cache_pkey'} = undef;
95             ( $rvalue, $msg ) = $self->SUPER::load_from_hash(@_);
96              
97             ## Check the return value, if its good, cache it!
98             $self->_store() if ($rvalue);
99             return ( $rvalue, $msg );
100             } else { # Called as a class method;
101             $self = $self->SUPER::load_from_hash(@_);
102             ## Check the return value, if its good, cache it!
103             $self->_store() if ( $self->id );
104             return ($self);
105             }
106              
107             }
108              
109             sub load_by_cols {
110             my ( $class, %attr ) = @_;
111              
112             my ($self);
113             if ( ref($class) ) {
114             ( $self, $class ) = ( $class, undef );
115             } else {
116             $self = $class->new(
117             handle => ( delete $attr{'_handle'} || undef ) );
118             }
119              
120             ## Generate the cache key
121             my $alt_key = $self->_gen_record_cache_key(%attr);
122             if ( $self->_fetch($alt_key) ) {
123             if ($class) { return $self }
124             else { return ( 1, "Fetched from cache" ) }
125             }
126              
127             # Blow away the primary cache key since we're loading.
128             $self->{'_jifty_cache_pkey'} = undef;
129              
130             ## Fetch from the DB!
131             my ( $rvalue, $msg ) = $self->SUPER::load_by_cols(%attr);
132             ## Check the return value, if its good, cache it!
133             if ($rvalue) {
134             ## Only cache the object if its okay to do so.
135             $self->_store();
136             $self->_key_cache->set(
137             $alt_key => $self->_primary_record_cache_key );
138              
139             }
140             if ($class) { return $self }
141             else {
142             return ( $rvalue, $msg );
143             }
144             }
145              
146             # Function: __set
147             # Type : (overloaded) public instance
148             # Args : see Jifty::DBI::Record::_Set
149             # Lvalue : ?
150              
151             sub __set () {
152             my $self = shift;
153              
154             $self->_expire();
155             return $self->SUPER::__set(@_);
156              
157             }
158              
159             # Function: delete
160             # Type : (overloaded) public instance
161             # Args : nil
162             # Lvalue : ?
163              
164             sub __delete () {
165             my $self = shift;
166              
167             $self->_expire();
168             return $self->SUPER::__delete(@_);
169              
170             }
171              
172             # Function: _expire
173             # Type : private instance
174             # Args : string(cache_key)
175             # Lvalue : 1
176             # Desc : Removes this object from the cache.
177              
178             sub _expire (\$) {
179             my $self = shift;
180             $self->_record_cache->set( $self->_primary_record_cache_key, undef, time - 1 );
181              
182             # We should be doing something more surgical to clean out the key cache. but we do need to expire it
183             $self->_flush_key_cache;
184              
185             }
186              
187             # Function: _fetch
188             # Type : private instance
189             # Args : string(cache_key)
190             # Lvalue : 1
191             # Desc : Get an object from the cache, and make this object that.
192              
193             sub _fetch () {
194             my ( $self, $cache_key ) = @_;
195             # If the alternate key is really the primary one
196            
197            
198             my $data = $self->_record_cache->fetch($cache_key);
199              
200             unless ($data) {
201             $cache_key = $self->_key_cache->fetch( $cache_key );
202             $data = $self->_record_cache->fetch( $cache_key ) if $cache_key;
203             }
204              
205             return undef unless ($data);
206              
207             @{$self}{ keys %$data } = values %$data; # deserialize
208             return 1;
209              
210              
211             }
212              
213             #sub __value {
214             # my $self = shift;
215             # my $column = shift;
216             #
217             # # XXX TODO, should we be fetching directly from the cache?
218             # return ( $self->SUPER::__value($column) );
219             #}
220              
221             # Function: _store
222             # Type : private instance
223             # Args : string(cache_key)
224             # Lvalue : 1
225             # Desc : Stores this object in the cache.
226              
227             sub _store (\$) {
228             my $self = shift;
229             $self->_record_cache->set( $self->_primary_record_cache_key,
230             { values => $self->{'values'},
231             table => $self->table,
232             fetched => $self->{'fetched'},
233             decoded => $self->{'decoded'},
234             }
235             );
236             }
237              
238              
239             # Function: _gen_record_cache_key
240             # Type : private instance
241             # Args : hash (attr)
242             # Lvalue : 1
243             # Desc : Takes a perl hash and generates a key from it.
244              
245             sub _gen_record_cache_key {
246             my ( $self, %attr ) = @_;
247              
248             my @cols;
249              
250             while ( my ( $key, $value ) = each %attr ) {
251             unless ( defined $value ) {
252             push @cols, lc($key) . '=__undef';
253             }
254             elsif ( ref($value) eq "HASH" ) {
255             push @cols, lc($key) . ( $value->{operator} || '=' )
256             . defined $value->{value}? $value->{value}: '__undef';
257             }
258             else {
259             push @cols, lc($key) . "=" . $value;
260             }
261             }
262             return ( $self->table() . ':' . join( ',', @cols ) );
263             }
264              
265             # Function: _fetch_record_cache_key
266             # Type : private instance
267             # Args : nil
268             # Lvalue : 1
269              
270             sub _fetch_record_cache_key {
271             my ($self) = @_;
272             my $cache_key = $self->_cache_config->{'cache_key'};
273             return ($cache_key);
274             }
275              
276             # Function: _primary_record_cache_key
277             # Type : private instance
278             # Args : none
279             # Lvalue: : 1
280             # Desc : generate a primary-key based variant of this object's cache key
281             # primary keys is in the cache
282              
283             sub _primary_record_cache_key {
284             my ($self) = @_;
285              
286             unless ( $self->{'_jifty_cache_pkey'} ) {
287              
288             my @attributes;
289             my %pk = $self->primary_keys;
290             while ( my ($key, $value) = each %pk ) {
291             return unless defined $value;
292             push @attributes, lc( $key ) . '=' . $value;
293             }
294              
295             $self->{'_jifty_cache_pkey'} = $self->table .':'
296             . join ',', @attributes;
297             }
298             return ( $self->{'_jifty_cache_pkey'} );
299              
300             }
301              
302             =head2 _cache_config
303              
304             You can override this method to change the duration of the caching
305             from the default of 5 seconds.
306              
307             For example, to cache records for up to 30 seconds, add the following
308             method to your class:
309              
310             sub _cache_config {
311             { 'cache_for_sec' => 30 }
312             }
313              
314             =cut
315              
316             sub _cache_config {
317             { 'cache_p' => 1,
318             'cache_for_sec' => 5,
319             };
320             }
321              
322             1;
323              
324             __END__
325              
326              
327             =head1 AUTHOR
328              
329             Matt Knopp <mhat@netlag.com>
330              
331             =head1 SEE ALSO
332              
333             L<Jifty::DBI>, L<Jifty::DBI::Record>
334              
335             =cut
336              
337