File Coverage

blib/lib/EntityModel/DB.pm
Criterion Covered Total %
statement 9 120 7.5
branch 0 22 0.0
condition 0 4 0.0
subroutine 3 14 21.4
pod 8 8 100.0
total 20 168 11.9


line stmt bran cond sub pod time code
1             package EntityModel::DB;
2             {
3             $EntityModel::DB::VERSION = '0.102';
4             }
5             # ABSTRACT: Database manager for entity handling
6             use EntityModel::Class {
7 16         331 user => { type => 'string' },
8             password => { type => 'string' },
9             host => { type => 'string' },
10             port => { type => 'string' },
11             dbname => { type => 'string' },
12             service => { type => 'string' },
13             pid => { type => 'int' },
14             transactionLevel => { type => 'int', default => 0 },
15 16     16   12349 };
  16         30  
16 16     16   12251 no if $] >= 5.017011, warnings => "experimental::smartmatch";
  16         58  
  16         122  
17              
18             =head1 NAME
19              
20             EntityModel::DB - database management
21              
22             =head1 VERSION
23              
24             version 0.102
25              
26             =head1 DESCRIPTION
27              
28             Manages database connections and transactions.
29              
30             =cut
31              
32 16     16   10784 use EntityModel::Query;
  16         51  
  16         3137  
33              
34             # Current database entry when in transaction
35             our $ACTIVE_DB;
36              
37             =head2 new
38              
39             Create a new L object.
40              
41             Does not attempt to connect any database handles, but prepares the context ready for the first
42             request.
43              
44             =cut
45              
46             sub new {
47 0     0 1   my $class = shift;
48 0           my $self = $class->SUPER::new(@_);
49 0           $self->pid($$);
50 0           return $self;
51             }
52              
53             =head2 dbh
54              
55             Returns a database handle.
56              
57             Can only be called within a transaction.
58              
59             =cut
60              
61             sub dbh {
62 0     0 1   my $self = shift;
63 0   0       my $name = shift // 'main';
64 0           $self->_fork_guard;
65              
66 0 0         return $self->{dbh}->{$name} if $self->{dbh}->{$name};
67              
68 0           logDebug("Connecting to database with DSN [%s]", $self->dsn);
69             # FIXME All this should go, it's supposed to be handled entirely by the backend
70             # storage engine.
71 0           require DBI;
72 0           my $dbh = $self->{dbh}->{$name} = DBI->connect(
73             $self->dsn,
74             $self->user,
75             $self->password, {
76             AutoCommit => 0,
77             RaiseError => 1,
78             PrintError => 0,
79             PrintWarn => 0,
80             # Turn off server-side prepare statements, since we want to support pgbouncer's transaction mode
81             pg_server_prepare => 0,
82             private_pid => $self->pid
83             }
84             );
85 0           return $dbh;
86             }
87              
88             =head2 dsn
89              
90             Data Source string used for connecting to the database.
91              
92             Currently hardcodes the dbi:Pg: prefix.
93              
94             =cut
95              
96             sub dsn {
97 0     0 1   my $self = shift;
98 0           my $dsn = "dbi:Pg:";
99 0           $dsn .= join(";", map { "$_=" . $self->$_ } grep { $self->$_ } qw{dbname host port service});
  0            
  0            
100 0           return $dsn;
101             }
102              
103             =head2 transaction
104              
105             Call code within a transaction.
106              
107             Note that this does not map exactly onto a single database transaction. Nested transactions are supported, using
108             savepoints, and a transaction may cover several active database handles.
109              
110             =cut
111              
112             sub transaction {
113 0     0 1   my $self = shift;
114 0           my $sub = shift;
115 0           $self->_fork_guard;
116              
117             # Record then increment current transaction level
118 0           my $level = $self->transactionLevel;
119 0           $self->transactionLevel($level + 1);
120              
121             # If we're already in a transaction, use a savepoint
122 0 0         if($level) {
123 0           logDebug("Savepoint %d", $level);
124 0           $self->dbh->do("savepoint tran_" . $self->transactionLevel);
125             }
126              
127             # Run the query, if this fails $status will be false
128             return try {
129 0           local $ACTIVE_DB = $self;
130 0           $sub->($self, @_);
131 0 0         die "Fork within transaction is not recommended" unless $self->pid ~~ $$;
132              
133 0 0         if($level) {
134 0           logDebug("Commit to level %d", $level);
135 0           $self->dbh->do("release tran_" . $self->transactionLevel);
136             } else {
137 0           logDebug("Commit");
138 0           $self->dbh->do("commit");
139             }
140             # Restore previous transaction level
141 0           $self->transactionLevel($level);
142 0           $self;
143 0           } catch {
144             # And for failure, do a rollback to previous level
145 0 0         if($level) {
146 0           logDebug("Rollback to level %d", $level);
147 0           $self->dbh->do("rollback to tran_" . $self->transactionLevel);
148             } else {
149 0           logDebug("Rollback");
150 0           $self->dbh->do("rollback");
151             }
152             # Restore previous transaction level
153 0           $self->transactionLevel($level);
154 0           logStack($_);
155 0           die $_;
156             };
157             }
158              
159             =head2 update
160              
161             Update information
162              
163             =cut
164              
165             sub update {
166 0     0 1   my $self = shift;
167 0           my %args = @_;
168 0           $self->_fork_guard;
169              
170 0           my $sql = $args{sql};
171 0           my $dbh = $self->dbh;
172 0           my $sth = $dbh->prepare($sql);
173 0           $sth->execute(@{$args{param}});
  0            
174 0 0         $args{on_complete}->($sth) if $args{on_complete};
175 0           return $sth;
176             }
177              
178             =head2 select
179              
180             Run a select query against the database and return the results as an orderly hash.
181              
182             =cut
183              
184             sub select : method {
185 0     0 1   my $self = shift;
186 0           my $sql = shift;
187 0           my $param = shift;
188 0           my %args = (
189             sql => $sql,
190             param => $param
191             );
192 0           my ($sth, $rslt) = $self->_run_query(%args);
193              
194 0           my @names = @{ $sth->{ NAME_lc } };
  0            
195 0           my @data;
196 0           foreach (@$rslt) {
197 0           my @row = @$_;
198 0           push @data, {
199             map {
200 0           $_ => shift(@row)
201             } @names
202             };
203             }
204 0           return \@data;
205             }
206              
207             =head2 select_iterator
208              
209             Run a select query against the database and return the results as an orderly hash.
210              
211             =cut
212              
213             sub select_iterator {
214 0     0 1   my $self = shift;
215 0           my %args = @_;
216 0 0         die "No method supplied" unless $args{method};
217              
218             # Set up the statement handle so we can read data
219 0           my ($sth, $rslt) = $self->_run_query(@_);
220              
221 0           my @names = @{ $sth->{ NAME_lc } };
  0            
222 0           my @data;
223 0           foreach (@$rslt) {
224 0           my @row = @$_;
225 0           my %data = map { $_ => shift(@row) } @names;
  0            
226 0           $args{method}->(\%data);
227             }
228 0           return 1;
229             }
230              
231             =head2 active_db
232              
233             Returns the currently active database handle.
234              
235             =cut
236              
237             sub active_db {
238 0     0 1   my $class = shift;
239 0           return $ACTIVE_DB;
240             }
241              
242             =head1 INTERNAL METHODS
243              
244             =cut
245              
246             =head2 _run_query
247              
248             Run the given query.
249              
250             =cut
251              
252             sub _run_query {
253 0     0     my $self = shift;
254 0           my %args = @_;
255 0           $self->_fork_guard;
256              
257 0           my $sql = $args{sql};
258 0           my $dbh = $self->dbh;
259             my $sth = try {
260 0           my $sth = $dbh->prepare($sql);
261 0           $sth->execute(@{$args{param}});
  0            
262 0           $sth;
263 0           } catch {
264 0           warn "$_\n";
265 0   0       die $_ // 'unknown error';
266             };
267 0           my $rslt;
268 0 0         $rslt = $sth->fetchall_arrayref if $sth->{Active};
269 0 0         return unless $rslt;
270 0           return ($sth, $rslt);
271             }
272              
273             =head2 _fork_guard
274              
275             Internal method used to check whether we've forked recently and if so reset the internal state
276             so that we don't try to reuse existing handles.
277              
278             =cut
279              
280             sub _fork_guard {
281 0     0     my $self = shift;
282 0 0         return $self if $self->pid ~~ $$;
283 0 0         logError("Fork inside a transaction (level %d), old pid %d, new pid %d", $self->transactionLevel, $self->pid, $$) if $self->transactionLevel;
284              
285 0           logDebug("Clean up after fork");
286 0           delete $self->{dbh};
287 0           $self->transactionLevel(0);
288 0           $self->pid($$);
289 0           return $self;
290             }
291              
292             sub DESTROY {
293 0     0     my $self = shift;
294 0           $self->_fork_guard;
295 0           $_->rollback foreach values %{$self->{dbh}};
  0            
296             }
297              
298             1;
299              
300             __END__