File Coverage

blib/lib/Catmandu/Store/DBI.pm
Criterion Covered Total %
statement 56 77 72.7
branch 9 24 37.5
condition 1 2 50.0
subroutine 13 18 72.2
pod 0 8 0.0
total 79 129 61.2


line stmt bran cond sub pod time code
1             package Catmandu::Store::DBI;
2              
3 6     6   162021 use Catmandu::Sane;
  6         1215369  
  6         53  
4 6     6   1853 use Catmandu::Util qw(require_package);
  6         15  
  6         343  
5 6     6   10084 use DBI;
  6         116729  
  6         450  
6 6     6   4076 use Catmandu::Store::DBI::Bag;
  6         26  
  6         267  
7 6     6   76 use Moo;
  6         17  
  6         31  
8 6     6   2393 use MooX::Aliases;
  6         16  
  6         58  
9 6     6   2910 use Catmandu::Error;
  6         15  
  6         190  
10 6     6   33 use namespace::clean;
  6         14  
  6         30  
11              
12             our $VERSION = "0.11";
13              
14             with 'Catmandu::Store';
15             with 'Catmandu::Transactional';
16              
17             has data_source => (
18             is => 'ro',
19             required => 1,
20             alias => 'dsn',
21             trigger => sub {
22             my $ds = $_[0]->{data_source};
23             $ds = $ds =~ /^DBI:/i ? $ds : "DBI:$ds";
24             $_[0]->{data_source} = $ds;
25             },
26             );
27             has username => (is => 'ro', default => sub {''}, alias => 'user');
28             has password => (is => 'ro', default => sub {''}, alias => 'pass');
29             has default_order => (is => 'ro', default => sub {'ID'});
30             has handler => (is => 'lazy');
31             has _in_transaction => (is => 'rw', writer => '_set_in_transaction',);
32             has _dbh => (is => 'lazy', builder => '_build_dbh', writer => '_set_dbh',);
33              
34             # DEPRECATED methods. Were only invented to tackle of problem of reconnection
35             sub timeout {
36 0     0 0 0 warn "method timeout has been replaced by auto reconnect";
37             }
38              
39             sub has_timeout {
40 0     0 0 0 warn "method has_timeout has been replaced by auto reconnect";
41 0         0 0;
42             }
43              
44             sub reconnect_after_timeout {
45 0     0 0 0 warn "method reconnect_after_timeout has been replaced by auto reconnect";
46             }
47              
48             sub handler_namespace {
49 5     5 0 16 'Catmandu::Store::DBI::Handler';
50             }
51              
52             sub _build_handler {
53 5     5   59 my ($self) = @_;
54 5   50     19 my $driver = $self->dbh->{Driver}{Name} // '';
55 5         250 my $ns = $self->handler_namespace;
56 5         8 my $pkg;
57 5 50       53 if ($driver =~ /pg/i) {
    50          
    0          
58 0         0 $pkg = 'Pg';
59             }
60             elsif ($driver =~ /sqlite/i) {
61 5         15 $pkg = 'SQLite';
62             }
63             elsif ($driver =~ /mysql/i) {
64 0         0 $pkg = 'MySQL';
65             }
66             else {
67 0         0 Catmandu::NotImplemented->throw(
68             'Only Pg, SQLite and MySQL are supported.');
69             }
70 5         30 require_package($pkg, $ns)->new;
71             }
72              
73             sub _build_dbh {
74 5     5   51 my ($self) = @_;
75 5         41 my $opts = {
76             AutoCommit => 1,
77             RaiseError => 1,
78             mysql_auto_reconnect => 1,
79             mysql_enable_utf8 => 1,
80             pg_utf8_strings => 1,
81             sqlite_use_immediate_transaction => 1,
82             sqlite_unicode => 1,
83             };
84 5         61 my $dbh
85             = DBI->connect($self->data_source, $self->username, $self->password,
86             $opts,);
87 5         10128 $dbh;
88             }
89              
90             sub dbh {
91              
92 102     102 0 1117 my $self = $_[0];
93 102         2041 my $dbh = $self->_dbh;
94              
95             # reconnect when dbh is not set (should never happen)
96 102 50       900 return $self->reconnect
97             unless defined $dbh;
98              
99             # check validity of dbh
100             # for performance reasons only check every second
101 102 100       279 if ( defined( $self->{last_ping_t} ) ) {
102              
103 97 50       479 return $dbh if (time - $self->{last_ping_t}) < 1;
104              
105             }
106              
107 5         16 $self->{last_ping_t} = time;
108 5 50       36 return $dbh if $dbh->ping;
109              
110             # one should never reconnect to a database during a transaction
111             # because that would initiate a new transaction
112             Catmandu::Error->throw("Connection to DBI backend lost, and cannot reconnect during a transaction")
113 0 0       0 unless $dbh->{AutoCommit};
114              
115             # reconnect and return dbh
116             # note: mysql_auto_reconnect only works when AutoCommit is 1
117 0         0 $self->reconnect;
118              
119             }
120              
121             sub reconnect {
122              
123 0     0 0 0 my $self = $_[0];
124 0         0 my $dbh = $self->_dbh;
125 0 0       0 $dbh->disconnect if defined($dbh);
126 0         0 $self->_set_dbh($self->_build_dbh);
127 0         0 $self->_dbh;
128              
129             }
130              
131             sub transaction {
132 1     1 0 46 my ($self, $sub) = @_;
133              
134 1 50       7 if ($self->_in_transaction) {
135 0         0 return $sub->();
136             }
137              
138 1         4 my $dbh = $self->dbh;
139 1         2 my @res;
140              
141             eval {
142 1         5 $self->_set_in_transaction(1);
143 1         14 $dbh->begin_work;
144 1         28 @res = $sub->();
145 0         0 $dbh->commit;
146 0         0 $self->_set_in_transaction(0);
147 0         0 1;
148 1 50       4 } or do {
149 1         19 my $err = $@;
150 1         2 eval {$dbh->rollback};
  1         512  
151 1         11 $self->_set_in_transaction(0);
152 1         11 die $err;
153             };
154              
155 0           @res;
156             }
157              
158             sub DEMOLISH {
159 0     0 0   my ($self) = @_;
160 0 0         $self->{_dbh}->disconnect if $self->{_dbh};
161             }
162              
163             1;
164              
165             __END__
166              
167             =pod
168              
169             =encoding utf8
170              
171             =head1 NAME
172              
173             Catmandu::Store::DBI - A Catmandu::Store backed by DBI
174              
175             =head1 VERSION
176              
177             Version 0.0424
178              
179             =head1 SYNOPSIS
180              
181             # From the command line
182             $ catmandu import JSON to DBI --data_source SQLite:mydb.sqlite < data.json
183              
184             # Or via a configuration file
185             $ cat catmandu.yml
186             ---
187             store:
188             mydb:
189             package: DBI
190             options:
191             data_source: "dbi:mysql:database=mydb"
192             username: xyz
193             password: xyz
194             ...
195             $ catmandu import JSON to mydb < data.json
196             $ catmandu export mydb to YAML > data.yml
197             $ catmandu export mydb --id 012E929E-FF44-11E6-B956-AE2804ED5190 to JSON > record.json
198             $ catmandu count mydb
199             $ catmandy delete mydb
200              
201             # From perl
202             use Catmandu::Store::DBI;
203              
204             my $store = Catmandu::Store::DBI->new(
205             data_source => 'DBI:mysql:database=mydb', # prefix "DBI:" optional
206             username => 'xyz', # optional
207             password => 'xyz', # optional
208             );
209              
210             my $obj1 = $store->bag->add({ name => 'Patrick' });
211              
212             printf "obj1 stored as %s\n" , $obj1->{_id};
213              
214             # Force an id in the store
215             my $obj2 = $store->bag->add({ _id => 'test123' , name => 'Nicolas' });
216              
217             my $obj3 = $store->bag->get('test123');
218              
219             $store->bag->delete('test123');
220              
221             $store->bag->delete_all;
222              
223             # All bags are iterators
224             $store->bag->each(sub { ... });
225             $store->bag->take(10)->each(sub { ... });
226              
227             =head1 DESCRIPTION
228              
229             A Catmandu::Store::DBI is a Perl package that can store data into
230             DBI backed databases. The database as a whole is a 'store'
231             L<Catmandu::Store>. Databases tables are 'bags' (L<Catmandu::Bag>).
232              
233             Databases need to be preconfigured for accepting Catmandu data. When
234             no specialized Catmandu tables exist in a database then Catmandu will
235             create them automatically. See "DATABASE CONFIGURATION" below.
236              
237             DO NOT USE Catmandu::Store::DBI on an existing database! Tables and
238             data can be deleted and changed.
239              
240             =head1 LIMITATIONS
241              
242             Currently only MySQL, Postgres and SQLite are supported. Text columns are also
243             assumed to be utf-8.
244              
245             =head1 CONFIGURATION
246              
247             =over
248              
249             =item data_source
250              
251             Required. The connection parameters to the database. See L<DBI> for more information.
252              
253             Examples:
254              
255             dbi:mysql:foobar <= a local mysql database 'foobar'
256             dbi:Pg:dbname=foobar;host=myserver.org;port=5432 <= a remote PostGres database
257             dbi:SQLite:mydb.sqlite <= a local SQLLite file based database mydb.sqlite
258             dbi:Oracle:host=myserver.org;sid=data01 <= a remote Oracle database
259              
260             Drivers for each database need to be available on your computer. Install then with:
261              
262             cpanm DBD::mysql
263             cpanm DBD::Pg
264             cpanm DBD::SQLite
265              
266             =item user
267              
268             Optional. A user name to connect to the database
269              
270             =item password
271              
272             Optional. A password for connecting to the database
273              
274             =item default_order
275              
276             Optional. Default the default sorting of results when returning an iterator.
277             Choose 'ID' to order on the configured identifier field, 'NONE' to skip all
278             ordering, or "$field" where $field is the name of a table column. By default
279             set to 'ID'.
280              
281             =back
282              
283             =head1 DATABASE CONFIGURATION
284              
285             When no tables exists for storing data in the database, then Catmandu
286             will create them. By default tables are created for each L<Catmandu::Bag>
287             which contain an '_id' and 'data' column.
288              
289             This behavior can be changed with mapping option:
290              
291             my $store = Catmandu::Store::DBI->new(
292             data_source => 'DBI:mysql:database=test',
293             bags => {
294             # books table
295             books => {
296             mapping => {
297             # these keys will be directly mapped to columns
298             # all other keys will be serialized in the data column
299             title => {type => 'string', required => 1, column => 'book_title'},
300             isbn => {type => 'string', unique => 1},
301             authors => {type => 'string', array => 1}
302             }
303             }
304             }
305             );
306              
307             For keys that have a corresponding table column configured, the method 'select' of class L<Catmandu::Store::DBI::Bag> provides
308             a more efficiënt way to query records.
309              
310             See L<Catmandu::Store::DBI::Bag> for more information.
311              
312             =head2 Column types
313              
314             =over
315              
316             =item string
317              
318             =item integer
319              
320             =item binary
321              
322             =item datetime
323              
324             Only MySQL, PostgreSQL
325              
326             =item datetime_milli
327              
328             Only MySQL, PostgreSQL
329              
330             =item json
331              
332             Only PostgreSQL
333              
334             This is mapped internally to postgres field of type "jsonb".
335              
336             Please use the serializer L<Catmandu::Serializer::json_string>,
337              
338             if you choose to store the perl data structure into this type of field.
339              
340             Reasons:
341              
342             * there are several types of serializers. E.g. serializer "messagepack"
343             produces a string that is not accepted by a jsonb field in postgres
344              
345             * the default serializer L<Catmandu::Serializer::json> converts the perl data structure to a binary json string,
346             and the DBI client reencodes that utf8 string (because jsonb is a sort of text field),
347             so you end up having a double encoded string.
348              
349             =back
350              
351             =head2 Column options
352              
353             =over
354              
355             =item column
356              
357             Name of the table column if it differs from the key in your data.
358              
359             =item array
360              
361             Boolean option, default is C<0>. Note that this is only supported for PostgreSQL.
362              
363             =item unique
364              
365             Boolean option, default is C<0>.
366              
367             =item index
368              
369             Boolean option, default is C<0>. Ignored if C<unique> is true.
370              
371             =item required
372              
373             Boolean option, default is C<0>.
374              
375             =back
376              
377             =head1 AUTO RECONNECT
378              
379             This library automatically connects to the underlying
380              
381             database, and reconnects when that connection is lost.
382              
383             There is one exception though: when the connection is lost
384              
385             in the middle of a transaction, this is skipped and
386              
387             a L<Catmandu::Error> is thrown. Reconnecting during a
388              
389             transaction would have returned a new transaction,
390              
391             and (probably?) committed the lost transaction
392              
393             contrary to your expectation. There is actually no way to
394              
395             recover from that, so throwing an error seemed
396              
397             liked to a "good" way to solve that.
398              
399              
400             In order to avoid this situation, try to avoid
401              
402             a big time lap between database actions during
403              
404             a transaction, as your server may have thrown
405              
406             you out.
407              
408             P.S. the mysql option C<< mysql_auto_reconnect >>
409              
410             does NOT automatically reconnect during a transaction
411              
412             exactly for this reason.
413              
414             =head1 SEE ALSO
415              
416             L<Catmandu::Bag>, L<DBI>
417              
418             =cut