File Coverage

blib/lib/Catmandu/Store/DBI.pm
Criterion Covered Total %
statement 21 68 30.8
branch 0 16 0.0
condition 0 13 0.0
subroutine 7 13 53.8
pod 0 4 0.0
total 28 114 24.5


line stmt bran cond sub pod time code
1             package Catmandu::Store::DBI;
2              
3 7     7   78974 use Catmandu::Sane;
  7         1293343  
  7         59  
4 7     7   2177 use Catmandu::Util qw(require_package);
  7         18  
  7         394  
5 7     7   11207 use DBI;
  7         125310  
  7         454  
6 7     7   4274 use Catmandu::Store::DBI::Bag;
  7         31  
  7         368  
7 7     7   75 use Moo;
  7         19  
  7         38  
8 7     7   2812 use MooX::Aliases;
  7         19  
  7         58  
9 7     7   3035 use namespace::clean;
  7         18  
  7         34  
10              
11             our $VERSION = "0.09";
12              
13             with 'Catmandu::Store';
14             with 'Catmandu::Transactional';
15              
16             has data_source => (
17             is => 'ro',
18             required => 1,
19             alias => 'dsn',
20             trigger => sub {
21             my $ds = $_[0]->{data_source};
22             $ds = $ds =~ /^DBI:/i ? $ds : "DBI:$ds";
23             $_[0]->{data_source} = $ds;
24             },
25             );
26             has username => (is => 'ro', default => sub {''}, alias => 'user');
27             has password => (is => 'ro', default => sub {''}, alias => 'pass');
28             has timeout => (is => 'ro', predicate => 1);
29             has reconnect_after_timeout => (is => 'ro');
30             has default_order => (is => 'ro', default => sub {'ID'});
31             has handler => (is => 'lazy');
32             has _in_transaction => (is => 'rw', writer => '_set_in_transaction',);
33             has _connect_time => (is => 'rw', writer => '_set_connect_time');
34             has _dbh => (is => 'lazy', builder => '_build_dbh', writer => '_set_dbh',);
35              
36             sub handler_namespace {
37 0     0 0   'Catmandu::Store::DBI::Handler';
38             }
39              
40             sub _build_handler {
41 0     0     my ($self) = @_;
42 0   0       my $driver = $self->dbh->{Driver}{Name} // '';
43 0           my $ns = $self->handler_namespace;
44 0           my $pkg;
45 0 0         if ($driver =~ /pg/i) {
    0          
    0          
46 0           $pkg = 'Pg';
47             }
48             elsif ($driver =~ /sqlite/i) {
49 0           $pkg = 'SQLite';
50             }
51             elsif ($driver =~ /mysql/i) {
52 0           $pkg = 'MySQL';
53             }
54             else {
55 0           Catmandu::NotImplemented->throw(
56             'Only Pg, SQLite and MySQL are supported.');
57             }
58 0           require_package($pkg, $ns)->new;
59             }
60              
61             sub _build_dbh {
62 0     0     my ($self) = @_;
63 0           my $opts = {
64             AutoCommit => 1,
65             RaiseError => 1,
66             mysql_auto_reconnect => 1,
67             mysql_enable_utf8 => 1,
68             pg_utf8_strings => 1,
69             sqlite_use_immediate_transaction => 1,
70             sqlite_unicode => 1,
71             };
72 0           my $dbh
73             = DBI->connect($self->data_source, $self->username, $self->password,
74             $opts,);
75 0           $self->_set_connect_time(time);
76 0           $dbh;
77             }
78              
79             sub dbh {
80 0     0 0   my ($self) = @_;
81 0           my $dbh = $self->_dbh;
82 0           my $connect_time = $self->_connect_time;
83 0   0       my $driver = $dbh->{Driver}{Name} // '';
84              
85             # MySQL has builtin option mysql_auto_reconnect
86 0 0 0       if ( $driver !~ /mysql/i
      0        
87             && $self->has_timeout
88             && time - $connect_time > $self->timeout)
89             {
90 0 0 0       if ($self->reconnect_after_timeout || !$dbh->ping) {
91              
92             # ping failed, so try to reconnect
93 0           $dbh->disconnect;
94 0           $dbh = $self->_build_dbh;
95 0           $self->_set_dbh($dbh);
96             }
97             else {
98 0           $self->_set_connect_time(time);
99             }
100             }
101              
102 0           $dbh;
103             }
104              
105             sub transaction {
106 0     0 0   my ($self, $sub) = @_;
107              
108 0 0         if ($self->_in_transaction) {
109 0           return $sub->();
110             }
111              
112 0           my $dbh = $self->dbh;
113 0           my @res;
114              
115             eval {
116 0           $self->_set_in_transaction(1);
117 0           $dbh->begin_work;
118 0           @res = $sub->();
119 0           $dbh->commit;
120 0           $self->_set_in_transaction(0);
121 0           1;
122 0 0         } or do {
123 0           my $err = $@;
124 0           eval {$dbh->rollback};
  0            
125 0           $self->_set_in_transaction(0);
126 0           die $err;
127             };
128              
129 0           @res;
130             }
131              
132             sub DEMOLISH {
133 0     0 0   my ($self) = @_;
134 0 0         $self->{_dbh}->disconnect if $self->{_dbh};
135             }
136              
137             1;
138              
139             __END__
140              
141             =pod
142              
143             =encoding utf8
144              
145             =head1 NAME
146              
147             Catmandu::Store::DBI - A Catmandu::Store backed by DBI
148              
149             =head1 VERSION
150              
151             Version 0.0424
152              
153             =head1 SYNOPSIS
154              
155             # From the command line
156             $ catmandu import JSON to DBI --data_source SQLite:mydb.sqlite < data.json
157              
158             # Or via a configuration file
159             $ cat catmandu.yml
160             ---
161             store:
162             mydb:
163             package: DBI
164             options:
165             data_source: "dbi:mysql:database=mydb"
166             username: xyz
167             password: xyz
168             ...
169             $ catmandu import JSON to mydb < data.json
170             $ catmandu export mydb to YAML > data.yml
171             $ catmandu export mydb --id 012E929E-FF44-11E6-B956-AE2804ED5190 to JSON > record.json
172             $ catmandu count mydb
173             $ catmandy delete mydb
174              
175             # From perl
176             use Catmandu::Store::DBI;
177              
178             my $store = Catmandu::Store::DBI->new(
179             data_source => 'DBI:mysql:database=mydb', # prefix "DBI:" optional
180             username => 'xyz', # optional
181             password => 'xyz', # optional
182             );
183              
184             my $obj1 = $store->bag->add({ name => 'Patrick' });
185              
186             printf "obj1 stored as %s\n" , $obj1->{_id};
187              
188             # Force an id in the store
189             my $obj2 = $store->bag->add({ _id => 'test123' , name => 'Nicolas' });
190              
191             my $obj3 = $store->bag->get('test123');
192              
193             $store->bag->delete('test123');
194              
195             $store->bag->delete_all;
196              
197             # All bags are iterators
198             $store->bag->each(sub { ... });
199             $store->bag->take(10)->each(sub { ... });
200              
201             =head1 DESCRIPTION
202              
203             A Catmandu::Store::DBI is a Perl package that can store data into
204             DBI backed databases. The database as a whole is a 'store'
205             L<Catmandu::Store>. Databases tables are 'bags' (L<Catmandu::Bag>).
206              
207             Databases need to be preconfigured for accepting Catmandu data. When
208             no specialized Catmandu tables exist in a database then Catmandu will
209             create them automatically. See "DATABASE CONFIGURATION" below.
210              
211             DO NOT USE Catmandu::Store::DBI on an existing database! Tables and
212             data can be deleted and changed.
213              
214             =head1 LIMITATIONS
215              
216             Currently only MySQL, Postgres and SQLite are supported. Text columns are also
217             assumed to be utf-8.
218              
219             =head1 CONFIGURATION
220              
221             =over
222              
223             =item data_source
224              
225             Required. The connection parameters to the database. See L<DBI> for more information.
226              
227             Examples:
228              
229             dbi:mysql:foobar <= a local mysql database 'foobar'
230             dbi:Pg:dbname=foobar;host=myserver.org;port=5432 <= a remote PostGres database
231             dbi:SQLite:mydb.sqlite <= a local SQLLite file based database mydb.sqlite
232             dbi:Oracle:host=myserver.org;sid=data01 <= a remote Oracle database
233              
234             Drivers for each database need to be available on your computer. Install then with:
235              
236             cpanm DBD::mysql
237             cpanm DBD::Pg
238             cpanm DBD::SQLite
239              
240             =item user
241              
242             Optional. A user name to connect to the database
243              
244             =item password
245              
246             Optional. A password for connecting to the database
247              
248             =item timeout
249              
250             Optional. Timeout for a inactive database handle. When timeout is reached, Catmandu
251             checks if the connection is still alive (by use of ping) or it recreates the connection.
252             See TIMEOUTS below for more information.
253              
254             =item reconnect_after_timeout
255              
256             Optional. When a timeout is reached, Catmandu reconnects to the database. By
257             default set to '0'
258              
259             =item default_order
260              
261             Optional. Default the default sorting of results when returning an iterator.
262             Choose 'ID' to order on the configured identifier field, 'NONE' to skip all
263             ordering, or "$field" where $field is the name of a table column. By default
264             set to 'ID'.
265              
266             =back
267              
268             =head1 DATABASE CONFIGURATION
269              
270             When no tables exists for storing data in the database, then Catmandu
271             will create them. By default tables are created for each L<Catmandu::Bag>
272             which contain an '_id' and 'data' column.
273              
274             This behavior can be changed with mapping option:
275              
276             my $store = Catmandu::Store::DBI->new(
277             data_source => 'DBI:mysql:database=test',
278             bags => {
279             # books table
280             books => {
281             mapping => {
282             # these keys will be directly mapped to columns
283             # all other keys will be serialized in the data column
284             title => {type => 'string', required => 1, column => 'book_title'},
285             isbn => {type => 'string', unique => 1},
286             authors => {type => 'string', array => 1}
287             }
288             }
289             }
290             );
291              
292             For keys that have a corresponding table column configured, the method 'select' of class L<Catmandu::Store::DBI::Bag> provides
293             a more efficiënt way to query records.
294              
295             See L<Catmandu::Store::DBI::Bag> for more information.
296              
297             =head2 Column types
298              
299             =over
300              
301             =item string
302              
303             =item integer
304              
305             =item binary
306              
307             =item datetime
308              
309             Only MySQL, PostgreSQL
310              
311             =item datetime_milli
312              
313             Only MySQL, PostgreSQL
314              
315             =item json
316              
317             Only PostgreSQL
318              
319             =back
320              
321             =head2 Column options
322              
323             =over
324              
325             =item column
326              
327             Name of the table column if it differs from the key in your data.
328              
329             =item array
330              
331             Boolean option, default is C<0>. Note that this is only supported for PostgreSQL.
332              
333             =item unique
334              
335             Boolean option, default is C<0>.
336              
337             =item index
338              
339             Boolean option, default is C<0>. Ignored if C<unique> is true.
340              
341             =item required
342              
343             Boolean option, default is C<0>.
344              
345             =back
346              
347             =head1 TIMEOUT
348              
349             It is a good practice to set the timeout high enough. When using transactions, one should avoid this situation:
350              
351             $bag->store->transaction(sub{
352             $bag->add({ _id => "1" });
353             sleep $timeout;
354             $bag->add({ _id => "2" });
355             });
356              
357             The following warning appears:
358              
359             commit ineffective with AutoCommit enabled at lib//Catmandu/Store/DBI.pm line 73.
360             DBD::SQLite::db commit failed: attempt to commit on inactive database handle
361              
362             This has the following reasons:
363              
364             1. first record added
365             2. timeout is reached, the connection is recreated
366             3. the option AutoCommit is set. So the database handle commits the current transaction. The first record is committed.
367             4. this new connection handle is used now. We're still in the method "transaction", but there is no longer a real transaction at database level.
368             5. second record is added (committed)
369             6. commit is issued. But this unnecessary, so the database handle throws a warning.
370              
371             =head1 SEE ALSO
372              
373             L<Catmandu::Bag>, L<DBI>
374              
375             =cut