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   2288 use Modern::Perl;
  1         3  
  1         10  
4 1     1   183 use Moo;
  1         3  
  1         6  
5 1     1   394 use Data::Dumper;
  1         3  
  1         61  
6 1     1   6 use Dancer2::Core::Types;
  1         3  
  1         9  
7 1     1   10402 use Dancer2::Plugin::Database;
  1         2  
  1         21  
8 1     1   180 use Carp qw(croak);
  1         2  
  1         55  
9 1     1   7 use Ref::Util qw(is_plain_hashref);
  1         2  
  1         50  
10 1     1   7 use Storable qw(nfreeze thaw);
  1         2  
  1         1775  
11             with 'Dancer2::Core::Role::SessionFactory';
12             our $VERSION="1.0013";
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 20 sub INSERT { 'INSERT into %s (%s,%s) values (?,?)' }
164              
165             sub create_flush_query {
166 1     1 0 118 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 24 sub UPDATE { 'UPDATE %s SET %s=? WHERE %s=?' }
183              
184             sub create_update_query {
185 1     1 0 4 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   24 sub DELETE { 'DELETE FROM %s WHERE %s=?' }
203              
204             sub create_destroy_query {
205 1     1 0 3 my ($self)=@_;
206 1         5 my $query=sprintf $self->DELETE,$self->session_table,$self->id_column;
207 1         31 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 25 sub SELECT { 'SELECT %s FROM %s WHERE %s=?' }
223              
224             sub create_retrieve_query {
225 1     1 0 583 my ($self)=@_;
226 1         4 my $query=sprintf $self->SELECT,$self->data_column,$self->session_table,$self->id_column;
227 1         51 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 25 sub SELECT_ALL { 'SELECT %s FROM %s' }
243              
244             sub create_sessions_query {
245 1     1 0 3 my ($self)=@_;
246 1         4 my $query=sprintf $self->SELECT_ALL,$self->id_column,$self->session_table;
247 1         30 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 24 sub RENAME { 'UPDATE %s SET %s=? WHERE %s=?' }
263              
264             sub create_change_query {
265 1     1 0 518 my ($self)=@_;
266 1         5 my $query=sprintf $self->RENAME,$self->session_table,$self->id_column,$self->id_column;
267 1         34 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 croak "Invalid session ID: $id"
314             if !defined $s;
315              
316 0         0 return $THAW->($s);
317             }
318              
319             sub _change_id {
320 0     0   0 my ( $self, $old_id, $new_id ) = @_;
321 0         0 my $sth=$self->get_sth('create_change_query');$HANDLE_EXECUTE->('create_change_query',$sth,$new_id,$old_id);
  0         0  
322             }
323              
324             sub _destroy {
325 0     0   0 my ( $self, $id ) = @_;
326              
327 0         0 my $sth=$self->get_sth('create_destroy_query');$HANDLE_EXECUTE->('create_destroy_query',$sth,$id);
  0         0  
328             }
329              
330             sub _flush {
331 0     0   0 my ( $self, $id, $data ) = @_;
332              
333 0 0       0 $data={} unless is_plain_hashref $data;
334            
335 0         0 my $s=$self->find_session($id);
336 0         0 my $string=$FREEZE->($data);
337            
338 0 0       0 if(defined($s)) {
339 0         0 my $sth=$self->get_sth('create_update_query');$HANDLE_EXECUTE->('create_update_query',$sth,$string,$id);
  0         0  
340             } else {
341 0         0 my $sth=$self->get_sth('create_flush_query');$HANDLE_EXECUTE->('create_flush_query',$sth,$id,$string);
  0         0  
342             }
343             }
344              
345             sub get_dbh {
346 0     0 0 0 my ($self)=@_;
347             #return Dancer2::Plugin::SessionDatabase::DBC($self->connection);
348 0         0 $self->execute_hook( 'engine.session.before_db', $self );
349              
350 0         0 return $self->dbh;
351             }
352              
353             =head1 hooks created
354              
355             This package supports the default session engine hooks along with the following addtional hooks documented in this section.
356              
357             =cut
358              
359             sub supported_hooks {
360 1     1 0 202 qw/
361             engine.session.before_retrieve
362             engine.session.after_retrieve
363              
364             engine.session.before_create
365             engine.session.after_create
366              
367             engine.session.before_change_id
368             engine.session.after_change_id
369              
370             engine.session.before_destroy
371             engine.session.after_destroy
372              
373             engine.session.before_flush
374             engine.session.after_flush
375              
376             engine.session.before_db
377             /;
378             }
379              
380             =head2 engine.session.before_db
381              
382             This hook is run before the session engine calls the database function from Dancer2::Plugin::Database.
383              
384             hook=>'engine.session.before_db'=>sub {
385             my ($session)=@_;
386             };
387              
388             Note: This hook is used by Dancer2::Plugin::SessionDatabase to set the database handle in the session object at runtime.
389              
390             =head1 hooks used in Dancer2::Plugin::Database
391              
392             This package makes use of hooks provdied by Dancer2::Database::Plugin.
393              
394             =head2 "database_connection_lost"
395              
396             This hook is used to clear the existing database statement handle cache.
397              
398             =head2 "database_error"
399              
400             This hook is used to clear the existing database statement handle cache.
401              
402             =head1 Notes
403              
404             =head2 Database Acces Pre-Fork
405              
406             If you access sessions preforking, you will need to reset the statement handle session cache.
407              
408             Example:
409              
410              
411             =head3 Clearing the Statement Handle Cache
412              
413             The following code snippit will reset the built in statement handle cache to empty.
414              
415             %{$Dancer2::Session::DatabasePlugin::CACHE}=();
416              
417             =head3 Clearing the Database Connection
418              
419             To release the current database session, use the following code snippet.
420              
421             $Dancer2::Plugin::SessionDatabase::DBH=undef;
422              
423             =head1 Specal Examples
424              
425             =head2 Changing the freeze and thaw functions
426              
427             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.
428              
429             =head3 Freeze
430              
431             The nfreeze code reference is stored here
432              
433             $Dancer2::Session::DatabasePlugin::FREEZE
434              
435             =head3 Thaw
436              
437             The thaw code reference is stored here
438              
439             $Dancer2::Session::DatabasePlugin::THAW
440              
441             =head2 Oracle in general
442              
443             Oracle has some odd quirks, here is an example configuration that may help solve more than a few problems.
444              
445             Database:
446             connections:
447             myoracledb:
448             driver: "Oracle:(DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = my.oracle.server.com)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME=ORACLE.SERVICE.COM)))"
449             username: OracleUser
450             password: 'xxxxxxx'
451             dbi_params:
452             RaiseError: 1
453             AutoCommit: 1
454             FetchHashKeyName: 'NAME_uc'
455             LongReadLen: 1000000
456              
457             =head2 The manual bind example ( Oracle and the like )
458              
459             Some databases require manual binds for blob. Here is an example of how to do this for Oracle.
460              
461             use DBD::Oracle qw(:ora_types);
462             use Dancer2;
463             use Dancer2::Plugin::Database;
464             use Dancer2::Plugin::SessionDatabase;
465              
466             $Dancer2::Session::DatabasePlugin::HANDLE_EXECUTE=sub {
467             my ($name,$sth,@bind)=@_;
468             if($name eq 'create_update_query') {
469             my ($string,$id)=@bind;
470             $sth->bind_param(1,$string,{ora_type => ORA_BLOB });
471             $sth->bind_param(2,$id,{ora_type => ORA_VARCHAR2});
472             $sth->execute();
473             } elsif($name eq 'create_flush_query') {
474             my ($id,$string)=@bind;
475             $sth->bind_param(1,$id,{ora_type => ORA_VARCHAR2});
476             $sth->bind_param(2,$string,{ora_type => ORA_BLOB });
477             $sth->execute();
478             } else {
479             $sth->execute(@bind);
480             }
481             };
482              
483             =head2 Completly Changing an SQL statement
484              
485             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.
486              
487             use Dancer2;
488             use Dancer2::Plugin::Database;
489             use Dancer2::Plugin::SessionDatabase;
490              
491             $Dancer2::Session::DatabasePlugin::HANDLE_SQL_STRING=sub {
492             my ($name)=@_;
493             if($name eq 'query_to_alter') {
494             $_[1]='some new sql statement';
495             }
496             };
497              
498             =head2 DBD::Sybase MSSQL FreeTDS Example
499              
500             This example represents how to deal with some of the strange limitations when connecting via MSSQL via DBD::Sybase with FreeTDS.
501              
502             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.
503              
504             =head3 Session Configuration
505              
506             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].
507              
508             engines:
509             session:
510             DatabasePlugin:
511             connection: "myconnection"
512             session_table: "SESSIONS"
513             id_column: "SESSION_ID"
514             data_column: "SESSION_DATA"
515             # Disable Caching of Statement handles
516             cache: 0
517             # skip internal Statment handler creation code for the following
518             no_create:
519             create_update_query: 1
520             create_flush_query: 1
521              
522             =head3 Database Configuration
523              
524             Our example database has AutoCommit Disabled.
525              
526             plugins:
527             Database:
528             connections:
529             socmon:
530             driver: Sybase
531             server: SOCMON_DEV
532             username: username
533             password: xxx
534             database: myconnection
535             dbi_params:
536             RaiseError: 1
537             AutoCommit: 1
538             FetchHashKeyName: 'NAME_lc'
539              
540             =head3 MSSQL Table Creation
541              
542             MSSQL has some odd quirks when it comes to binary data, so in this case we will use varchar(max).
543              
544             create table SESSIONS (
545             session_id varchar(32) ,
546             session_data varchar(max),
547             l astUpdate TimeStamp,
548             CONSTRAINT AK_session_id UNIQUE(session_id)
549             )
550              
551             =head3 Code Example
552              
553             Finnaly in your Dancer2 App we add the following code.
554              
555             use JSON qw(to_json from_jsom);
556              
557             $Dancer2::Session::DatabasePlugin::FREEZE=\&to_json;
558             $Dancer2::Session::DatabasePlugin::THAW=\&from_json;
559              
560             $Dancer2::Session::DatabasePlugin::HANDLE_EXECUTE=sub {
561             my ($name,$sth,@bind)=@_;
562             if($name eq 'create_update_query') {
563             my ($string,$id)=@bind;
564             $string=~ s/'/''/g;
565             $id=~ s/'/''/g;
566             $Dancer2::Plugin::SessionDatabase::DBH->do("update sessions set session_data='$string' where session_id='$id'");
567             } elsif($name eq 'create_flush_query') {
568             my ($id,$string)=@bind;
569             $string=~ s/'/''/g;
570             $id=~ s/'/''/g;
571             $Dancer2::Plugin::SessionDatabase::DBH->do("insert into sessions (session_data,session_id) values ('$string','$id')");
572             } else {
573             $sth->execute(@bind);
574             }
575             };
576              
577             =head1 See Also
578              
579             Dancer2::Plugin::Database
580             Dancer2::Session::YAML
581              
582             =head1 LICENSE
583              
584             This softare is distributed under the Perl 5 License.
585              
586             =head1 AUTHOR
587              
588             Michael Shipper <AKALINUX@cpan.org>
589              
590             =cut
591              
592             1;