M::DBIC::Schema -- refactored helper, caching support
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Model / DBIC / Schema.pm
1 package Catalyst::Model::DBIC::Schema;
2
3 use strict;
4 use warnings;
5 no warnings 'uninitialized';
6
7 our $VERSION = '0.24';
8
9 use parent qw/Catalyst::Model Class::Accessor::Fast Class::Data::Accessor/;
10 use MRO::Compat;
11 use mro 'c3';
12 use UNIVERSAL::require;
13 use Carp;
14 use Data::Dumper;
15 use DBIx::Class ();
16 use Scalar::Util 'reftype';
17 use namespace::clean -except => 'meta';
18
19 __PACKAGE__->mk_classaccessor('composed_schema');
20 __PACKAGE__->mk_accessors(qw/
21     schema connect_info schema_class storage_type caching model_name
22 /);
23
24 =head1 NAME
25
26 Catalyst::Model::DBIC::Schema - DBIx::Class::Schema Model Class
27
28 =head1 SYNOPSIS
29
30 Manual creation of a DBIx::Class::Schema and a Catalyst::Model::DBIC::Schema:
31
32 =over
33
34 =item 1.
35
36 Create the DBIx:Class schema in MyApp/Schema/FilmDB.pm:
37
38   package MyApp::Schema::FilmDB;
39   use base qw/DBIx::Class::Schema/;
40
41   __PACKAGE__->load_classes(qw/Actor Role/);
42
43 =item 2.
44
45 Create some classes for the tables in the database, for example an 
46 Actor in MyApp/Schema/FilmDB/Actor.pm:
47
48   package MyApp::Schema::FilmDB::Actor;
49   use base qw/DBIx::Class/
50
51   __PACKAGE__->load_components(qw/Core/);
52   __PACKAGE__->table('actor');
53
54   ...
55
56 and a Role in MyApp/Schema/FilmDB/Role.pm:
57
58   package MyApp::Schema::FilmDB::Role;
59   use base qw/DBIx::Class/
60
61   __PACKAGE__->load_components(qw/Core/);
62   __PACKAGE__->table('role');
63
64   ...    
65
66 Notice that the schema is in MyApp::Schema, not in MyApp::Model. This way it's 
67 usable as a standalone module and you can test/run it without Catalyst. 
68
69 =item 3.
70
71 To expose it to Catalyst as a model, you should create a DBIC Model in
72 MyApp/Model/FilmDB.pm:
73
74   package MyApp::Model::FilmDB;
75   use base qw/Catalyst::Model::DBIC::Schema/;
76
77   __PACKAGE__->config(
78       schema_class => 'MyApp::Schema::FilmDB',
79       connect_info => {
80                         dsn => "DBI:...",
81                         user => "username",
82                         password => "password",
83                       }
84   );
85
86 See below for a full list of the possible config parameters.
87
88 =back
89
90 Now you have a working Model which accesses your separate DBIC Schema. This can
91 be used/accessed in the normal Catalyst manner, via $c->model():
92
93   my $actor = $c->model('FilmDB::Actor')->find(1);
94
95 You can also use it to set up DBIC authentication with 
96 Authentication::Store::DBIC in MyApp.pm:
97
98   package MyApp;
99
100   use Catalyst qw/... Authentication::Store::DBIC/;
101
102   ...
103
104   __PACKAGE__->config->{authentication}{dbic} = {
105       user_class      => 'FilmDB::Actor',
106       user_field      => 'name',
107       password_field  => 'password'
108   }
109
110 C<< $c->model('Schema::Source') >> returns a L<DBIx::Class::ResultSet> for 
111 the source name parameter passed. To find out more about which methods can 
112 be called on a ResultSet, or how to add your own methods to it, please see 
113 the ResultSet documentation in the L<DBIx::Class> distribution.
114
115 Some examples are given below:
116
117   # to access schema methods directly:
118   $c->model('FilmDB')->schema->source(...);
119
120   # to access the source object, resultset, and class:
121   $c->model('FilmDB')->source(...);
122   $c->model('FilmDB')->resultset(...);
123   $c->model('FilmDB')->class(...);
124
125   # For resultsets, there's an even quicker shortcut:
126   $c->model('FilmDB::Actor')
127   # is the same as $c->model('FilmDB')->resultset('Actor')
128
129   # To get the composed schema for making new connections:
130   my $newconn = $c->model('FilmDB')->composed_schema->connect(...);
131
132   # Or the same thing via a convenience shortcut:
133   my $newconn = $c->model('FilmDB')->connect(...);
134
135   # or, if your schema works on different storage drivers:
136   my $newconn = $c->model('FilmDB')->composed_schema->clone();
137   $newconn->storage_type('::LDAP');
138   $newconn->connection(...);
139
140   # and again, a convenience shortcut
141   my $newconn = $c->model('FilmDB')->clone();
142   $newconn->storage_type('::LDAP');
143   $newconn->connection(...);
144
145 =head1 DESCRIPTION
146
147 This is a Catalyst Model for L<DBIx::Class::Schema>-based Models.  See
148 the documentation for L<Catalyst::Helper::Model::DBIC::Schema> for
149 information on generating these Models via Helper scripts.
150
151 When your Catalyst app starts up, a thin Model layer is created as an 
152 interface to your DBIC Schema. It should be clearly noted that the model 
153 object returned by C<< $c->model('FilmDB') >> is NOT itself a DBIC schema or 
154 resultset object, but merely a wrapper proving L<methods|/METHODS> to access 
155 the underlying schema. 
156
157 In addition to this model class, a shortcut class is generated for each 
158 source in the schema, allowing easy and direct access to a resultset of the 
159 corresponding type. These generated classes are even thinner than the model 
160 class, providing no public methods but simply hooking into Catalyst's 
161 model() accessor via the 
162 L<ACCEPT_CONTEXT|Catalyst::Component/ACCEPT_CONTEXT> mechanism. The complete 
163 contents of each generated class is roughly equivalent to the following:
164
165   package MyApp::Model::FilmDB::Actor
166   sub ACCEPT_CONTEXT {
167       my ($self, $c) = @_;
168       $c->model('FilmDB')->resultset('Actor');
169   }
170
171 In short, there are three techniques available for obtaining a DBIC 
172 resultset object: 
173
174   # the long way
175   my $rs = $c->model('FilmDB')->schema->resultset('Actor');
176
177   # using the shortcut method on the model object
178   my $rs = $c->model('FilmDB')->resultset('Actor');
179
180   # using the generated class directly
181   my $rs = $c->model('FilmDB::Actor');
182
183 In order to add methods to a DBIC resultset, you cannot simply add them to 
184 the source (row, table) definition class; you must define a separate custom 
185 resultset class. See L<DBIx::Class::Manual::Cookbook/"Predefined searches"> 
186 for more info.
187
188 =head1 CONFIG PARAMETERS
189
190 =over 4
191
192 =item schema_class
193
194 This is the classname of your L<DBIx::Class::Schema> Schema.  It needs
195 to be findable in C<@INC>, but it does not need to be inside the 
196 C<Catalyst::Model::> namespace.  This parameter is required.
197
198 =item connect_info
199
200 This is an arrayref of connection parameters, which are specific to your
201 C<storage_type> (see your storage type documentation for more details). 
202 If you only need one parameter (e.g. the DSN), you can just pass a string 
203 instead of an arrayref.
204
205 This is not required if C<schema_class> already has connection information
206 defined inside itself (which isn't highly recommended, but can be done)
207
208 For L<DBIx::Class::Storage::DBI>, which is the only supported
209 C<storage_type> in L<DBIx::Class> at the time of this writing, the
210 parameters are your dsn, username, password, and connect options hashref.
211
212 See L<DBIx::Class::Storage::DBI/connect_info> for a detailed explanation
213 of the arguments supported.
214
215 Examples:
216
217   connect_info => {
218     dsn => 'dbi:Pg:dbname=mypgdb',
219     user => 'postgres',
220     password => ''
221   }
222
223   connect_info => {
224     dsn => 'dbi:SQLite:dbname=foo.db',
225     on_connect_do => [
226       'PRAGMA synchronous = OFF',
227     ]
228   }
229
230   connect_info => {
231     dsn => 'dbi:Pg:dbname=mypgdb',
232     user => 'postgres',
233     password => '',
234     pg_enable_utf8 => 1,
235     on_connect_do => [
236       'some SQL statement',
237       'another SQL statement',
238     ],
239   }
240
241 Or using L<Config::General>:
242
243     <Model::FilmDB>
244         schema_class   MyApp::Schema::FilmDB
245         <connect_info>
246             dsn   dbi:Pg:dbname=mypgdb
247             user   postgres
248             password ''
249             auto_savepoint 1
250             on_connect_do   some SQL statement
251             on_connect_do   another SQL statement
252         </connect_info>
253     </Model::FilmDB>
254
255 or
256
257     <Model::FilmDB>
258         schema_class   MyApp::Schema::FilmDB
259         connect_info   dbi:SQLite:dbname=foo.db
260     </Model::FilmDB>
261
262 Or using L<YAML>:
263
264   Model::MyDB:
265       schema_class: MyDB
266       connect_info:
267           dsn: dbi:Oracle:mydb
268           user: mtfnpy
269           password: mypass
270           LongReadLen: 1000000
271           LongTruncOk: 1
272           on_connect_do: [ "alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'" ]
273           cursor_class: 'DBIx::Class::Cursor::Cached'
274
275 The old arrayref style with hashrefs for L<DBI> then L<DBIx::Class> options is also
276 supported:
277
278   connect_info => [
279     'dbi:Pg:dbname=mypgdb',
280     'postgres',
281     '',
282     {
283       pg_enable_utf8 => 1,
284     },
285     {
286       on_connect_do => [
287         'some SQL statement',
288         'another SQL statement',
289       ],
290     }
291   ]
292
293 =item caching
294
295 Whether or not to enable caching support using L<DBIx::Class::Cursor::Cached>
296 and L<Catalyst::Plugin::Cache>. Enabled by default.
297
298 In order for this to work, L<Catalyst::Plugin::Cache> must be configured and
299 loaded. A possible configuration would look like this:
300
301   <Plugin::Cache>
302     <backend>       
303       class Cache::FastMmap
304       unlink_on_exit 1
305     </backend>
306   </Plugin::Cache>
307
308 Then in your queries, set the C<cache_for> ResultSet attribute to the number of
309 seconds you want the query results to be cached for, eg.:
310
311   $c->model('DB::Table')->search({ foo => 'bar' }, { cache_for => 18000 });
312
313 =item storage_type
314
315 Allows the use of a different C<storage_type> than what is set in your
316 C<schema_class> (which in turn defaults to C<::DBI> if not set in current
317 L<DBIx::Class>).  Completely optional, and probably unnecessary for most
318 people until other storage backends become available for L<DBIx::Class>.
319
320 =back
321
322 =head1 METHODS
323
324 =over 4
325
326 =item new
327
328 Instantiates the Model based on the above-documented ->config parameters.
329 The only required parameter is C<schema_class>.  C<connect_info> is
330 required in the case that C<schema_class> does not already have connection
331 information defined for it.
332
333 =item schema
334
335 Accessor which returns the connected schema being used by the this model.
336 There are direct shortcuts on the model class itself for
337 schema->resultset, schema->source, and schema->class.
338
339 =item composed_schema
340
341 Accessor which returns the composed schema, which has no connection info,
342 which was used in constructing the C<schema> above.  Useful for creating
343 new connections based on the same schema/model.  There are direct shortcuts
344 from the model object for composed_schema->clone and composed_schema->connect
345
346 =item clone
347
348 Shortcut for ->composed_schema->clone
349
350 =item connect
351
352 Shortcut for ->composed_schema->connect
353
354 =item source
355
356 Shortcut for ->schema->source
357
358 =item class
359
360 Shortcut for ->schema->class
361
362 =item resultset
363
364 Shortcut for ->schema->resultset
365
366 =item storage
367
368 Provides an accessor for the connected schema's storage object.
369 Used often for debugging and controlling transactions.
370
371 =cut
372
373 sub new {
374     my $self = shift->next::method(@_);
375     
376     my $class = ref $self;
377
378     $self->_build_model_name;
379
380     croak "->config->{schema_class} must be defined for this model"
381         unless $self->schema_class;
382
383     my $schema_class = $self->schema_class;
384
385     $schema_class->require
386         or croak "Cannot load schema class '$schema_class': $@";
387
388     if( !$self->connect_info ) {
389         if($schema_class->storage && $schema_class->storage->connect_info) {
390             $self->connect_info($schema_class->storage->connect_info);
391         }
392         else {
393             croak "Either ->config->{connect_info} must be defined for $class"
394                   . " or $schema_class must have connect info defined on it."
395                   . " Here's what we got:\n"
396                   . Dumper($self);
397         }
398     }
399
400     $self->composed_schema($schema_class->compose_namespace($class));
401
402     $self->schema($self->composed_schema->clone);
403
404     $self->schema->storage_type($self->storage_type)
405         if $self->storage_type;
406
407     $self->_normalize_connect_info;
408
409     $self->_setup_caching;
410
411     $self->schema->connection($self->connect_info);
412
413     $self->_install_rs_models;
414
415     return $self;
416 }
417
418 sub clone { shift->composed_schema->clone(@_); }
419
420 sub connect { shift->composed_schema->connect(@_); }
421
422 sub storage { shift->schema->storage(@_); }
423
424 =item ACCEPT_CONTEXT
425
426 Sets up runtime cache support on $c->model invocation.
427
428 =cut
429
430 sub ACCEPT_CONTEXT {
431     my ($self, $c) = @_;
432
433     return $self unless 
434         $self->caching;
435     
436     unless ($c->can('cache') && ref $c->cache) {
437         $c->log->debug("DBIx::Class cursor caching disabled, you don't seem to"
438             . " have a working Cache plugin.");
439         $self->caching(0);
440         $self->_reset_cursor_class;
441         return $self;
442     }
443
444     if (ref $self->schema->default_resultset_attributes) {
445         $self->schema->default_resultset_attributes->{cache_object} =
446             $c->cache;
447     } else {
448         $self->schema->default_resultset_attributes({
449             cache_object => $c->cache
450         });
451     }
452
453     $self;
454 }
455
456 sub _normalize_connect_info {
457     my $self = shift;
458
459     my $connect_info = $self->connect_info;
460
461     my @connect_info = reftype $connect_info eq 'ARRAY' ?
462         @$connect_info : $connect_info;
463
464     my %connect_info;
465
466     if (!ref $connect_info[0]) { # array style
467         @connect_info{qw/dsn user password/} =
468             splice @connect_info, 0, 3;
469
470         for my $i (0..1) {
471             my $extra = shift @connect_info;
472             last unless $extra;
473             croak "invalid connect_info" unless reftype $extra eq 'HASH';
474
475             %connect_info = (%connect_info, %$extra);
476         }
477
478         croak "invalid connect_info" if @connect_info;
479     } elsif (@connect_info == 1 && reftype $connect_info[0] eq 'HASH') {
480         %connect_info = %{ $connect_info[0] };
481     } elsif (reftype $connect_info eq 'HASH') {
482         %connect_info = %$connect_info;
483     } else {
484         croak "invalid connect_info";
485     }
486
487     if (exists $connect_info{cursor_class}) {
488         $connect_info{cursor_class}->require
489             or croak "invalid connect_info: Cannot load your cursor_class"
490                      . " $connect_info{cursor_class}: $@";
491     }
492
493     $self->connect_info(\%connect_info);
494 }
495
496 sub _install_rs_models {
497     my $self  = shift;
498     my $class = ref $self;
499
500     no strict 'refs';
501     foreach my $moniker ($self->schema->sources) {
502         my $classname = "${class}::$moniker";
503         *{"${classname}::ACCEPT_CONTEXT"} = sub {
504             shift;
505             shift->model($self->model_name)->resultset($moniker);
506         }
507     }
508 }
509
510 sub _build_model_name {
511     my $self = shift;
512
513     my $class = ref $self;
514     my $model_name = $class;
515     $model_name =~ s/^[\w:]+::(?:Model|M):://;
516
517     $self->model_name($model_name);
518 }
519
520 sub _setup_caching {
521     my $self = shift;
522
523     return if defined $self->caching && !$self->caching;
524
525     $self->caching(0);
526
527     if (my $cursor_class = $self->connect_info->{cursor_class}) {
528         unless ($cursor_class->can('clear_cache')) {
529             carp "Caching disabled, cursor_class $cursor_class does not"
530                  . " support it.";
531             return;
532         }
533     } else {
534         my $cursor_class = 'DBIx::Class::Cursor::Cached';
535
536         unless ($cursor_class->require) {
537             carp "Caching disabled, cannot load $cursor_class: $@";
538             return;
539         }
540
541         $self->connect_info->{cursor_class} = $cursor_class;
542     }
543
544     $self->caching(1);
545
546     1;
547 }
548
549 sub _reset_cursor_class {
550     my $self = shift;
551
552     if ($self->connect_info->{cursor_class} eq 'DBIx::Class::Cursor::Cached') {
553         $self->storage->cursor_class('DBIx::Class::Storage::DBI::Cursor');
554     }
555     
556     1;
557 }
558
559 =back
560
561 =head1 SEE ALSO
562
563 General Catalyst Stuff:
564
565 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
566 L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
567
568 Stuff related to DBIC and this Model style:
569
570 L<DBIx::Class>, L<DBIx::Class::Schema>,
571 L<DBIx::Class::Schema::Loader>, L<Catalyst::Helper::Model::DBIC::Schema>
572
573 =head1 AUTHOR
574
575 Brandon L Black, C<blblack@gmail.com>
576
577 =head1 COPYRIGHT
578
579 This program is free software, you can redistribute it and/or modify it
580 under the same terms as Perl itself.
581
582 =cut
583
584 1;