File Coverage

blib/lib/Dancer2/Session/DatabasePlugin.pm
Criterion Covered Total %
statement 47 96 48.9
branch 0 16 0.0
condition 0 3 0.0
subroutine 21 31 67.7
pod 1 17 5.8
total 69 163 42.3


line stmt bran cond sub pod time code
1             package Dancer2::Session::DatabasePlugin;
2              
3 1     1   1857 use Modern::Perl;
  1         3  
  1         8  
4 1     1   159 use Moo;
  1         2  
  1         6  
5 1     1   318 use Data::Dumper;
  1         2  
  1         59  
6 1     1   6 use Dancer2::Core::Types;
  1         2  
  1         7  
7 1     1   8289 use Dancer2::Plugin::Database;
  1         2  
  1         8  
8 1     1   143 use Carp qw(croak);
  1         2  
  1         44  
9 1     1   5 use Ref::Util qw(is_plain_hashref);
  1         2  
  1         35  
10 1     1   4 use Storable qw(nfreeze thaw);
  1         3  
  1         1445  
11             with 'Dancer2::Core::Role::SessionFactory';
12             our $VERSION="1.0014";
13              
14             our $HANDLE_SQL_STRING=\&stub_function;
15             our $HANDLE_EXECUTE=\&handle_execute;
16       0 0   sub stub_function { }
17              
18             sub handle_execute {
19 0     0 0 0 my ($name,$sth,@args)=@_;
20 0         0 $sth->execute(@args);
21             }
22              
23             our $CACHE={};
24              
25             our $FREEZE=\&nfreeze;
26             our $THAW=\&thaw;
27              
28             has no_create=>(
29             ias=>HashRef,
30             is=>'ro',
31             default=>sub { return {}},
32             );
33              
34             has cache =>(
35             isa=>Bool,
36             is=>'rw',
37             default=>0,
38             );
39              
40             has cache_sth=>(
41             isa=>Bool,
42             is=>'ro',
43             default=>0,
44             );
45              
46             has sth_cache=>(
47             isa=>HashRef,
48             default=>sub { $CACHE },
49             is=>'ro',
50             );
51              
52             has connection=>(
53             isa=>Str,
54             is=>'rw',
55             default=>'foo',
56             required=>1,
57             );
58              
59             has session_table=>(
60             isa=>Str,
61             required=>1,
62             is=>'rw',
63             default=>'SESSIONS',
64             );
65              
66             has id_column=>(
67             isa=>Str,
68             required=>1,
69             is=>'rw',
70             default=>'SESSION_ID',
71             );
72              
73             has data_column=>(
74             isa=>Str,
75             required=>1,
76             is=>'rw',
77             default=>'SESSION_DATA',
78             );
79              
80             has dbh=>(
81             is=>'rw',
82             );
83              
84             =head1 NAME
85              
86             Dancer2::Session::DatabasePlugin - Dancer2 Session implementation for databases
87              
88             =head1 SYNOPSIS
89              
90             use Dancer2;
91             use Dancer2::Plugin::Database;
92             use Dancer2::Plugin::SessionDatabase;
93              
94             =head1 DESCRIPTION
95              
96             This class extends Dancer2::Core::Role::SessionFactory, and makes use of Dancer2::Plugin::Database for managing database connections.
97              
98             =head1 CONFIGURATION
99              
100             The session should be set to "DatabasePlugin" in order to use this session engine in your Dancer2 Application.
101              
102             session: "DatabasePlugin"
103              
104             engines:
105             session:
106             DatabasePlugin:
107             cache: 0 # default 0, when 1 statement handles are cached
108             connection: "foo"
109             session_table: "SESSIONS"
110             id_column: "SESSION_ID"
111             data_column: "SESSION_DATA"
112             cache_sth: 0 # default 0, when set to 1 statement handles are cached
113              
114             plugins:
115             Database:
116             connections:
117             foo:
118             driver: "SQLite"
119             database: "foo.sqlite"
120              
121             =head1 Expected Schema
122              
123             The code was developed to use a table with 2 columns: SESSION_ID, SESSION_DATA, additional columns will not impact the code. No records are deleted unless the session destroy is called, so cleanup is something that may need to be done over time.
124              
125             The sql statements are generated based on the configuration options, session_table, id_column, and data_column.
126              
127             =head2 Example Schema
128              
129             Testing and development was done using SQLite3.
130              
131             Create statement is as follows:
132              
133             create table sessions (session_id varchar unique,session_data blob);
134              
135             =head1 How Queries are generated
136              
137             All queries are generated using sprintf statements against constatins.
138              
139             =head2 Column SESSION_ID
140              
141             This column must have constraint defining the values as unique. The id is a string representing the current session, internals from Dancer2::Core::Session seems to return a 32 byte long string. It is highly recommended this column be indexed.
142              
143             =head2 Column SESSION_DATA
144              
145             This field is expected to be a BLOB or binary data type, although a large text field should work. The data being written to this column is generated by using Storable::nfreeze($ref).
146              
147             =head1 SQL Statements
148              
149             All SQL Statements are generated based on the given configuration.
150              
151             =head2 Insert
152              
153             Default Query Shown:
154              
155             INSERT into SESSIONS (SESSION_ID,SESSION_DATA) values (?,?)
156              
157             Sprintf Template:
158              
159             INSERT into %s (%s,%s) values (?,?)
160              
161             =cut
162              
163 1     1 0 16 sub INSERT { 'INSERT into %s (%s,%s) values (?,?)' }
164              
165             sub create_flush_query {
166 1     1 0 103 my ($self)=@_;
167 1         4 return sprintf $self->INSERT,$self->session_table,$self->id_column,$self->data_column;
168             }
169              
170             =head2 Update Existing session
171              
172             Default Query Shown:
173              
174             UPDATE SESSIONS SET SESSION_DATA=? WHERE SESSION_ID=?
175              
176             Sprintf Template:
177              
178             UPDATE %s SET %s=? WHERE %s=?
179              
180             =cut
181              
182 1     1 0 20 sub UPDATE { 'UPDATE %s SET %s=? WHERE %s=?' }
183              
184             sub create_update_query {
185 1     1 0 2 my ($self)=@_;
186              
187 1         4 my $query=sprintf $self->UPDATE,$self->session_table,$self->data_column,$self->id_column;
188             }
189              
190             =head2 Delete
191              
192             Default Query Shown:
193              
194             DELETE FROM SESSIONS WHERE SESSION_ID=?
195              
196             Sprintf Template:
197              
198             DELETE FROM %s WHERE %s=?
199              
200             =cut
201              
202 1     1   21 sub DELETE { 'DELETE FROM %s WHERE %s=?' }
203              
204             sub create_destroy_query {
205 1     1 0 3 my ($self)=@_;
206 1         3 my $query=sprintf $self->DELETE,$self->session_table,$self->id_column;
207 1         25 return $query;
208             }
209              
210             =head2 SELECT Current Session
211              
212             Default Query Shown:
213              
214             SELECT SESSION_DATA FROM SESSIONS WHERE SESSION_ID=?
215              
216             Sprintf Template:
217              
218             SELECT %s FROM %s WHERE %s=?
219              
220             =cut
221              
222 1     1 1 21 sub SELECT { 'SELECT %s FROM %s WHERE %s=?' }
223              
224             sub create_retrieve_query {
225 1     1 0 481 my ($self)=@_;
226 1         4 my $query=sprintf $self->SELECT,$self->data_column,$self->session_table,$self->id_column;
227 1         39 return $query;
228             }
229              
230             =head2 SELECT All Session Keys
231              
232             Default Query Shown:
233              
234             SELECT SESSION_ID FROM SESSIONS
235              
236             Sprintf Template
237              
238             SELECT %s FROM %s
239              
240             =cut
241              
242 1     1 0 20 sub SELECT_ALL { 'SELECT %s FROM %s' }
243              
244             sub create_sessions_query {
245 1     1 0 4 my ($self)=@_;
246 1         4 my $query=sprintf $self->SELECT_ALL,$self->id_column,$self->session_table;
247 1         25 return $query;
248             }
249              
250             =head2 Rename Session
251              
252             Default Query Shown:
253              
254             UPDATE SESSIONS SET SESSION_ID=? WHERE SESSION_ID=?
255              
256             Sprintf Template:
257              
258             UPDATE %s SET %s=? WHERE %s=?
259              
260             =cut
261              
262 1     1 0 21 sub RENAME { 'UPDATE %s SET %s=? WHERE %s=?' }
263              
264             sub create_change_query {
265 1     1 0 425 my ($self)=@_;
266 1         4 my $query=sprintf $self->RENAME,$self->session_table,$self->id_column,$self->id_column;
267 1         27 return $query;
268             }
269              
270             sub get_sth($) {
271 0     0 0 0 my ($self,$method)=@_;
272              
273 0 0       0 if($self->no_create->{$method}) {
274 0         0 return undef;
275             }
276 0 0 0     0 return $self->sth_cache->{$method} if $self->cache && exists $self->sth_cache->{$method};
277              
278 0         0 my $query=$self->$method;
279 0         0 my $sth;
280 0         0 $HANDLE_SQL_STRING->($method,$query,$self->get_dbh,$sth);
281 0 0       0 $sth=$self->get_dbh->prepare($query) unless defined($sth);
282              
283             # only cache the statement handle if we are told too
284 0 0       0 return $sth unless $self->cache_sth;
285 0 0       0 return $sth unless $self->cache;
286 0         0 return $self->sth_cache->{$method}=$sth;
287             }
288              
289             sub _sessions {
290 0     0   0 my ($self) = @_;
291 0         0 my $data=[];
292 0         0 my $sth=$self->get_sth('create_sessions_query');$HANDLE_EXECUTE->('create_sessions_query',$sth,);
  0         0  
293              
294 0         0 while(my $row=$sth->fetchtow_arrayref) {
295 0         0 push @{$data},@{$row};
  0         0  
  0         0  
296             }
297              
298 0         0 return $data;
299             }
300              
301             sub find_session {
302 0     0 0 0 my ( $self, $id ) = @_;
303              
304 0         0 my $sth=$self->get_sth('create_retrieve_query');$HANDLE_EXECUTE->('create_retrieve_query',$sth,$id);
  0         0  
305 0         0 my ($s)=$sth->fetchrow_array;
306 0         0 return $s;
307             }
308              
309             sub _retrieve {
310 0     0   0 my ( $self, $id ) = @_;
311 0         0 my $s=$self->find_session($id);
312            
313 0 0       0 return undef unless defined($s);
314              
315 0         0 return $THAW->($s);
316             }
317              
318             sub _change_id {
319 0     0   0 my ( $self, $old_id, $new_id ) = @_;
320 0         0 my $sth=$self->get_sth('create_change_query');$HANDLE_EXECUTE->('create_change_query',$sth,$new_id,$old_id);
  0         0  
321             }
322              
323             sub _destroy {
324 0     0   0 my ( $self, $id ) = @_;
325              
326 0         0 my $sth=$self->get_sth('create_destroy_query');$HANDLE_EXECUTE->('create_destroy_query',$sth,$id);
  0         0  
327             }
328              
329             sub _flush {
330 0     0   0 my ( $self, $id, $data ) = @_;
331              
332 0 0       0 $data={} unless is_plain_hashref $data;
333            
334 0         0 my $s=$self->find_session($id);
335 0         0 my $string=$FREEZE->($data);
336            
337 0 0       0 if(defined($s)) {
338 0         0 my $sth=$self->get_sth('create_update_query');$HANDLE_EXECUTE->('create_update_query',$sth,$string,$id);
  0         0  
339             } else {
340 0         0 my $sth=$self->get_sth('create_flush_query');$HANDLE_EXECUTE->('create_flush_query',$sth,$id,$string);
  0         0  
341             }
342             }
343              
344             sub get_dbh {
345 0     0 0 0 my ($self)=@_;
346             #return Dancer2::Plugin::SessionDatabase::DBC($self->connection);
347 0         0 $self->execute_hook( 'engine.session.before_db', $self );
348              
349 0         0 return $self->dbh;
350             }
351              
352             =head1 hooks created
353              
354             This package supports the default session engine hooks along with the following addtional hooks documented in this section.
355              
356             =cut
357              
358             sub supported_hooks {
359 1     1 0 145 qw/
360             engine.session.before_retrieve
361             engine.session.after_retrieve
362              
363             engine.session.before_create
364             engine.session.after_create
365              
366             engine.session.before_change_id
367             engine.session.after_change_id
368              
369             engine.session.before_destroy
370             engine.session.after_destroy
371              
372             engine.session.before_flush
373             engine.session.after_flush
374              
375             engine.session.before_db
376             /;
377             }
378              
379             =head2 engine.session.before_db
380              
381             This hook is run before the session engine calls the database function from Dancer2::Plugin::Database.
382              
383             hook=>'engine.session.before_db'=>sub {
384             my ($session)=@_;
385             };
386              
387             Note: This hook is used by Dancer2::Plugin::SessionDatabase to set the database handle in the session object at runtime.
388              
389             =head1 hooks used in Dancer2::Plugin::Database
390              
391             This package makes use of hooks provdied by Dancer2::Database::Plugin.
392              
393             =head2 "database_connection_lost"
394              
395             This hook is used to clear the existing database statement handle cache.
396              
397             =head2 "database_error"
398              
399             This hook is used to clear the existing database statement handle cache.
400              
401             =head1 Notes
402              
403             =head2 Database Acces Pre-Fork
404              
405             If you access sessions preforking, you will need to reset the statement handle session cache.
406              
407             Example:
408              
409              
410             =head3 Clearing the Statement Handle Cache
411              
412             The following code snippit will reset the built in statement handle cache to empty.
413              
414             %{$Dancer2::Session::DatabasePlugin::CACHE}=();
415              
416             =head3 Clearing the Database Connection
417              
418             To release the current database session, use the following code snippet.
419              
420             $Dancer2::Plugin::SessionDatabase::DBH=undef;
421              
422             =head1 Specal Examples
423              
424             =head2 Changing the freeze and thaw functions
425              
426             Your database may not support globs or glob syntax, when this is the case it is possible to set a new subrouteens in place that handle the freezing and thawing of data.
427              
428             =head3 Freeze
429              
430             The nfreeze code reference is stored here
431              
432             $Dancer2::Session::DatabasePlugin::FREEZE
433              
434             =head3 Thaw
435              
436             The thaw code reference is stored here
437              
438             $Dancer2::Session::DatabasePlugin::THAW
439              
440             =head2 Oracle in general
441              
442             Oracle has some odd quirks, here is an example configuration that may help solve more than a few problems.
443              
444             Database:
445             connections:
446             myoracledb:
447             driver: "Oracle:(DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = my.oracle.server.com)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME=ORACLE.SERVICE.COM)))"
448             username: OracleUser
449             password: 'xxxxxxx'
450             dbi_params:
451             RaiseError: 1
452             AutoCommit: 1
453             FetchHashKeyName: 'NAME_uc'
454             LongReadLen: 1000000
455              
456             =head2 The manual bind example ( Oracle and the like )
457              
458             Some databases require manual binds for blob. Here is an example of how to do this for Oracle.
459              
460             use DBD::Oracle qw(:ora_types);
461             use Dancer2;
462             use Dancer2::Plugin::Database;
463             use Dancer2::Plugin::SessionDatabase;
464              
465             $Dancer2::Session::DatabasePlugin::HANDLE_EXECUTE=sub {
466             my ($name,$sth,@bind)=@_;
467             if($name eq 'create_update_query') {
468             my ($string,$id)=@bind;
469             $sth->bind_param(1,$string,{ora_type => ORA_BLOB });
470             $sth->bind_param(2,$id,{ora_type => ORA_VARCHAR2});
471             $sth->execute();
472             } elsif($name eq 'create_flush_query') {
473             my ($id,$string)=@bind;
474             $sth->bind_param(1,$id,{ora_type => ORA_VARCHAR2});
475             $sth->bind_param(2,$string,{ora_type => ORA_BLOB });
476             $sth->execute();
477             } else {
478             $sth->execute(@bind);
479             }
480             };
481              
482             =head2 Completly Changing an SQL statement
483              
484             Sometimes you may want to replace the query created with something entierly new. To do this you will need to set $HANDLE_SQL_STRING function refrerence.
485              
486             use Dancer2;
487             use Dancer2::Plugin::Database;
488             use Dancer2::Plugin::SessionDatabase;
489              
490             $Dancer2::Session::DatabasePlugin::HANDLE_SQL_STRING=sub {
491             my ($name)=@_;
492             if($name eq 'query_to_alter') {
493             $_[1]='some new sql statement';
494             }
495             };
496              
497             =head2 DBD::Sybase MSSQL FreeTDS Example
498              
499             This example represents how to deal with some of the strange limitations when connecting via MSSQL via DBD::Sybase with FreeTDS.
500              
501             The limitations are as follows: DBD::Sybase does not support multiple open statement handls when AuttoCommit is true. DBD::Sybase doesn't handle placeholders properly, and has some issues with binary data as well.
502              
503             =head3 Session Configuration
504              
505             In our session configuration we need to do the following: Disable statement handle caching and turn off the standard query generation code for the following functions: [create_update_query,create_flush_query].
506              
507             engines:
508             session:
509             DatabasePlugin:
510             connection: "myconnection"
511             session_table: "SESSIONS"
512             id_column: "SESSION_ID"
513             data_column: "SESSION_DATA"
514             # Disable Caching of Statement handles
515             cache: 0
516             # skip internal Statment handler creation code for the following
517             no_create:
518             create_update_query: 1
519             create_flush_query: 1
520              
521             =head3 Database Configuration
522              
523             Our example database has AutoCommit Disabled.
524              
525             plugins:
526             Database:
527             connections:
528             socmon:
529             driver: Sybase
530             server: SOCMON_DEV
531             username: username
532             password: xxx
533             database: myconnection
534             dbi_params:
535             RaiseError: 1
536             AutoCommit: 1
537             FetchHashKeyName: 'NAME_lc'
538              
539             =head3 MSSQL Table Creation
540              
541             MSSQL has some odd quirks when it comes to binary data, so in this case we will use varchar(max).
542              
543             create table SESSIONS (
544             session_id varchar(32) ,
545             session_data varchar(max),
546             l astUpdate TimeStamp,
547             CONSTRAINT AK_session_id UNIQUE(session_id)
548             )
549              
550             =head3 Code Example
551              
552             Finnaly in your Dancer2 App we add the following code.
553              
554             use JSON qw(to_json from_jsom);
555              
556             $Dancer2::Session::DatabasePlugin::FREEZE=\&to_json;
557             $Dancer2::Session::DatabasePlugin::THAW=\&from_json;
558              
559             $Dancer2::Session::DatabasePlugin::HANDLE_EXECUTE=sub {
560             my ($name,$sth,@bind)=@_;
561             if($name eq 'create_update_query') {
562             my ($string,$id)=@bind;
563             $string=~ s/'/''/g;
564             $id=~ s/'/''/g;
565             $Dancer2::Plugin::SessionDatabase::DBH->do("update sessions set session_data='$string' where session_id='$id'");
566             } elsif($name eq 'create_flush_query') {
567             my ($id,$string)=@bind;
568             $string=~ s/'/''/g;
569             $id=~ s/'/''/g;
570             $Dancer2::Plugin::SessionDatabase::DBH->do("insert into sessions (session_data,session_id) values ('$string','$id')");
571             } else {
572             $sth->execute(@bind);
573             }
574             };
575              
576             =head1 See Also
577              
578             Dancer2::Plugin::Database
579             Dancer2::Session::YAML
580              
581             =head1 LICENSE
582              
583             This softare is distributed under the Perl 5 License.
584              
585             =head1 AUTHOR
586              
587             Michael Shipper <AKALINUX@cpan.org>
588              
589             =cut
590              
591             1;