root/trunk/lib/Data/ObjectDriver/Driver/DBI.pm @ 97

Revision 97, 13.6 kB (checked in by sky, 4 years ago)

r100@crucially-3 (orig r1083): mpaschal | 2006-01-11 11:43:39 -0800
Allow extra DBI->connect options with Driver->new( connect_options => {} )
Add missing ) in synopsis found while looking for a place to document connect_options (which there isn't)

  • Property svn:keywords set to Id Revision
Line 
1# $Id$
2
3package Data::ObjectDriver::Driver::DBI;
4use strict;
5use base qw( Data::ObjectDriver Class::Accessor::Fast );
6
7use DBI;
8use Carp ();
9use Data::ObjectDriver::SQL;
10use Data::ObjectDriver::Driver::DBD;
11
12__PACKAGE__->mk_accessors(qw( dsn username password connect_options dbh get_dbh dbd ));
13
14sub init {
15    my $driver = shift;
16    my %param = @_;
17    for my $key (keys %param) {
18        $driver->$key($param{$key});
19    }
20    if(!exists $param{dbd}) {
21        ## Create a DSN-specific driver (e.g. "mysql").
22        my $type;
23        if (my $dsn = $driver->dsn) {
24            ($type) = $dsn =~ /^dbi:(\w*)/;
25        } elsif (my $dbh = $driver->dbh) {
26            $type = $dbh->{Driver}{Name};
27        } elsif (my $getter = $driver->get_dbh) {
28## Ugly. Shouldn't have to connect just to get the driver name.
29            my $dbh = $getter->();
30            $type = $dbh->{Driver}{Name};
31        }
32        $driver->dbd(Data::ObjectDriver::Driver::DBD->new($type));
33    }
34    $driver;
35}
36
37sub generate_pk {
38    my $driver = shift;
39    if (my $generator = $driver->pk_generator) {
40        return $generator->(@_);
41    }
42}
43
44sub init_db {
45    my $driver = shift;
46    my $dbh;
47    eval {
48        local $SIG{ALRM} = sub { die "alarm\n" };
49        $dbh = DBI->connect($driver->dsn, $driver->username, $driver->password,
50            { RaiseError => 1, PrintError => 0, AutoCommit => 1,
51              %{$driver->connect_options || {}} })
52            or Carp::croak("Connection error: " . $DBI::errstr);
53        alarm 0;
54    };
55    if ($@) {
56        Carp::croak(@$ eq "alarm\n" ? "Connection timeout" : $@);
57    }
58    $driver->dbd->init_dbh($dbh);
59    $dbh;
60}
61
62sub rw_handle {
63    my $driver = shift;
64    my $db = shift || 'main';
65    my $dbh = $driver->dbh;
66    unless ($dbh) {
67        if (my $getter = $driver->get_dbh) {
68            $dbh = $getter->();
69        } else {
70            $dbh = $driver->init_db($db) or die $driver->errstr;
71            $driver->dbh($dbh);
72        }
73    }
74    $dbh;
75}
76*r_handle = \&rw_handle;
77
78sub fetch_data {
79    my $driver = shift;
80    my($obj) = @_;
81    return unless $obj->has_primary_key;
82    my $terms = $driver->primary_key_to_terms(ref($obj), $obj->primary_key);
83    my $args  = { limit => 1 };
84    my $rec = {};
85    my $sth = $driver->fetch($rec, $obj, $terms, $args);
86    $sth->fetch;
87    $sth->finish;
88    return $rec;
89}
90
91sub fetch {
92    my $driver = shift;
93    my($rec, $class, $orig_terms, $orig_args) = @_;
94   
95    ## Use (shallow) duplicates so the pre_search trigger can modify them.
96    my $terms = defined $orig_terms ? { %$orig_terms } : undef;
97    my $args  = defined $orig_args  ? { %$orig_args  } : undef;
98    $class->call_trigger('pre_search', $terms, $args);
99
100    my $stmt = $driver->prepare_statement($class, $terms, $args);
101
102    my @bind;
103    my $map = $stmt->select_map;
104    for my $col (@{ $stmt->select }) {
105        push @bind, \$rec->{ $map->{$col} };
106    }
107
108    my $sql = $stmt->as_sql;
109    my $dbh = $driver->r_handle($class->properties->{db});
110    $driver->debug($sql, $stmt->{bind});
111    my $sth = $dbh->prepare_cached($sql);
112    $sth->execute(@{ $stmt->{bind} });
113    $sth->bind_columns(undef, @bind);
114
115    # need to slurp 'offset' rows for DBs that cannot do it themselves
116    if (!$driver->dbd->offset_implemented && $args->{offset}) {
117        for (1..$args->{offset}) {
118            $sth->fetch;
119        }
120    }
121
122    # xxx what happens if $sth goes out of scope without finish() being called ?
123    $sth;
124}
125
126sub search {
127    my($driver) = shift;
128    my($class, $terms, $args) = @_;
129
130    my $rec = {};
131    my $sth = $driver->fetch($rec, $class, $terms, $args);
132
133    my $iter = sub {
134        ## This is kind of a hack--we need $driver to stay in scope,
135        ## so that the DESTROY method isn't called. So we include it
136        ## in the scope of the closure.
137        my $d = $driver;
138
139        unless ($sth->fetch) {
140            $sth->finish;
141            return;
142        }
143        my $obj;
144        $obj = $class->new;
145        $obj->set_values($rec);
146        ## Don't need a duplicate as there's no previous version in memory
147        ## to preserve.
148        $obj->call_trigger('post_load');
149        $obj;
150    };
151   
152    if (wantarray) {
153        my @objs;
154        while (my $obj = $iter->()) {
155            push @objs, $obj;
156        }
157        return @objs;
158    } else {
159        return $iter;
160    }
161}
162
163sub is_same_array {
164    my($a1, $a2) = @_;
165    return if ($#$a1 != $#$a2);
166    for (my $i = 0; $i <= $#$a1; $i++) {
167        return if $a1->[$i] ne $a2->[$i];
168    }
169    return 1;
170}
171
172sub primary_key_to_terms {
173    my $driver = shift;
174    my($class, $id) = @_;
175    my $pk = $class->primary_key_tuple;
176    if (ref($id) eq 'HASH') {
177        my @keys = sort keys %$id;
178        unless (is_same_array(\@keys, [ sort @$pk ])) {
179            Carp::croak("keys don't match with primary keys: @keys");
180        }
181        return $id;
182    }
183
184    $id = [ $id ] unless ref($id) eq 'ARRAY';
185    my $i = 0;
186    my %terms;
187    @terms{@$pk} = @$id;
188    \%terms;
189}
190
191sub lookup {
192    my $driver = shift;
193    my($class, $id) = @_;
194    my @obj = $driver->search($class,
195        $driver->primary_key_to_terms($class, $id), { limit => 1 });
196    $obj[0];
197}
198
199## xxx refactor to use an OR search
200sub lookup_multi {
201    my $driver = shift;
202    my($class, $ids) = @_;
203    my @got;
204    for my $id (@$ids) {
205        push @got, $driver->lookup($class, $id);
206    }
207    \@got;
208}
209
210sub select_one {
211    my $driver = shift;
212    my($sql, $bind) = @_;
213    my $dbh = $driver->r_handle;
214    my $sth = $dbh->prepare_cached($sql);
215    $sth->execute(@$bind);
216    $sth->bind_columns(undef, \my($val));
217    $sth->fetch or return;
218    $sth->finish;
219    $val;
220}
221
222sub exists {
223    my $driver = shift;
224    my($obj) = @_;
225    return unless $obj->has_primary_key;
226    my $tbl = $obj->datasource;
227    my $stmt = $driver->prepare_statement(ref($obj),
228        $driver->primary_key_to_terms(ref($obj), $obj->primary_key),
229        { limit => 1 });
230    my $sql = "SELECT 1 FROM $tbl\n";
231    $sql .= $stmt->as_sql_where;
232    my $dbh = $driver->r_handle($obj->properties->{db});
233    $driver->debug($sql, $stmt->{bind});
234    my $sth = $dbh->prepare_cached($sql);
235    $sth->execute(@{ $stmt->{bind} });
236    my $exists = $sth->fetch;
237    $sth->finish;
238    $exists;
239}
240
241sub insert {
242    my $driver = shift;
243    my($orig_obj) = @_;
244
245    ## Use a duplicate so the pre_save trigger can modify it.
246    my $obj = $orig_obj->clone;
247    $obj->call_trigger('pre_save');
248    $obj->call_trigger('pre_insert');
249   
250    my $cols = $obj->column_names;
251    unless ($obj->has_primary_key) {
252        ## If we don't already have a primary key assigned for this object, we
253        ## may need to generate one (depending on the underlying DB
254        ## driver). If the driver gives us a new ID, we insert that into
255        ## the new record; otherwise, we assume that the DB is using an
256        ## auto-increment column of some sort, so we don't specify an ID
257        ## at all.
258        my $pk = $obj->primary_key_tuple;
259        if(my $generated = $driver->generate_pk($obj)) {
260            ## The ID is the only thing we *are* allowed to change on
261            ## the original object.
262            $orig_obj->$_($obj->$_) for @$pk;
263        } else {
264            my %pk = map { $_ => 1 } @$pk;
265            $cols = [ grep !$pk{$_} || defined $obj->$_(), @$cols ];
266        }
267    }
268    my $tbl = $obj->datasource;
269    my $sql = "INSERT INTO $tbl\n";
270    my $dbd = $driver->dbd;
271    $sql .= '(' . join(', ',
272                  map $dbd->db_column_name($tbl, $_),
273                  @$cols) .
274            ')' . "\n" .
275            'VALUES (' . join(', ', ('?') x @$cols) . ')' . "\n";
276    my $dbh = $driver->rw_handle($obj->properties->{db});
277    $driver->debug($sql, $obj->{column_values});
278    my $sth = $dbh->prepare_cached($sql);
279    my $i = 1;
280    my $col_defs = $obj->properties->{column_defs};
281    for my $col (@$cols) {
282        my $val = $obj->column($col);
283        my $type = $col_defs->{$col} || 'char';
284        my $attr = $dbd->bind_param_attributes($type);
285        $sth->bind_param($i++, $val, $attr);
286    }
287    $sth->execute;
288    $sth->finish;
289
290    ## Now, if we didn't have an object ID, we need to grab the
291    ## newly-assigned ID.
292    unless ($obj->has_primary_key) {
293        my $pk = $obj->primary_key_tuple;
294        my $id_col = $pk->[0]; # XXX are we sure we will always use '0' ?
295        my $id = $dbd->fetch_id(ref($obj), $dbh, $sth);
296        $obj->$id_col($id);
297        ## The ID is the only thing we *are* allowed to change on
298        ## the original object.
299        $orig_obj->$id_col($id);
300    }
301
302    $obj->call_trigger('post_save');
303    $obj->call_trigger('post_insert');
304    1;
305}
306
307sub update {
308    my $driver = shift;
309    my($orig_obj) = @_;
310
311    ## Use a duplicate so the pre_save trigger can modify it.
312    my $obj = $orig_obj->clone;
313    $obj->call_trigger('pre_save');
314    $obj->call_trigger('pre_update');
315
316    my $cols = $obj->column_names;
317    my $pk = $obj->primary_key_tuple;
318    my %pk = map { $_ => 1 } @$pk;
319    $cols = [ grep !$pk{$_}, @$cols ];
320
321    ## If there's no non-PK column, update() is no-op
322    @$cols or return 1;
323
324    my $tbl = $obj->datasource;
325    my $sql = "UPDATE $tbl SET\n";
326    my $dbd = $driver->dbd;
327    $sql .= join(', ',
328            map $dbd->db_column_name($tbl, $_) . " = ?",
329            @$cols) . "\n";
330    my $stmt = $driver->prepare_statement(ref($obj),
331        $driver->primary_key_to_terms(ref($obj), $obj->primary_key));
332    $sql .= $stmt->as_sql_where;
333   
334    my $dbh = $driver->rw_handle($obj->properties->{db});
335    $driver->debug($sql, $obj->{column_values});
336    my $sth = $dbh->prepare_cached($sql);
337    my $i = 1;
338    my $col_defs = $obj->properties->{column_defs};
339    for my $col (@$cols) {
340        my $val = $obj->column($col);
341        my $type = $col_defs->{$col} || 'char';
342        my $attr = $dbd->bind_param_attributes($type);
343        $sth->bind_param($i++, $val, $attr);
344    }
345
346    ## Bind the primary key value(s).
347    for my $val (@{ $stmt->{bind} }) {
348        $sth->bind_param($i++, $val);
349    }
350
351    $sth->execute;
352    $sth->finish;
353
354    $obj->call_trigger('post_save');
355    $obj->call_trigger('post_update');
356    1;
357}
358
359sub remove {
360    my $driver = shift;
361    my $orig_obj = shift;
362
363    ## If remove() is called on class method and we have 'nofetch'
364    ## option, we remove the record using $term and won't create
365    ## $object. This is for efficiency and PK-less tables
366    ## Note: In this case, triggers won't be fired
367    ## Otherwise, Class->remove is a shortcut for search+remove
368    unless (ref($orig_obj)) {
369        if ($_[1] && $_[1]->{nofetch}) {
370            return $driver->direct_remove($orig_obj, @_);
371        } else {
372            my @obj = $driver->search($orig_obj, @_);
373            for my $obj (@obj) {
374                $obj->remove;
375            }
376            return 1;
377        }
378    }
379   
380    return unless $orig_obj->has_primary_key;
381
382    ## Use a duplicate so the pre_save trigger can modify it.
383    my $obj = $orig_obj->clone;
384    $obj->call_trigger('pre_save');
385    $obj->call_trigger('pre_remove');
386
387    my $tbl = $obj->datasource;
388    my $sql = "DELETE FROM $tbl\n";
389    my $stmt = $driver->prepare_statement(ref($obj),
390        $driver->primary_key_to_terms(ref($obj), $obj->primary_key));
391    $sql .= $stmt->as_sql_where;
392    my $dbh = $driver->rw_handle($obj->properties->{db});
393    $driver->debug($sql, $stmt->{bind});
394    my $sth = $dbh->prepare_cached($sql);
395    $sth->execute(@{ $stmt->{bind} });
396    $sth->finish;
397
398    $obj->call_trigger('post_remove');
399   
400    1;
401}
402
403sub direct_remove {
404    my $driver = shift;
405    my($class, $orig_terms, $orig_args) = @_;
406
407    ## Use (shallow) duplicates so the pre_search trigger can modify them.
408    my $terms = defined $orig_terms ? { %$orig_terms } : undef;
409    my $args  = defined $orig_args  ? { %$orig_args  } : undef;
410    $class->call_trigger('pre_search', $terms, $args);
411
412    my $stmt = $driver->prepare_statement($class, $terms, $args);
413    my $tbl  = $class->datasource;
414    my $sql  = "DELETE from $tbl\n";
415       $sql .= $stmt->as_sql_where;
416
417    my $dbh = $driver->rw_handle($class->properties->{db});
418    $driver->debug($sql, $stmt->{bind});
419    my $sth = $dbh->prepare_cached($sql);
420    $sth->execute(@{ $stmt->{bind} });
421    $sth->finish;
422
423    1;
424}
425
426sub commit {
427    my $driver = shift;
428    if (my $dbh = $driver->dbh) {
429        $dbh->commit;
430    }
431    1;
432}
433
434sub rollback {
435    my $driver = shift;
436    if (my $dbh = $driver->dbh) {
437        $dbh->rollback;
438    }
439    1;
440}
441
442sub DESTROY {
443    if (my $dbh = shift->dbh) {
444        $dbh->disconnect if $dbh;
445    }
446}
447
448sub prepare_statement {
449    my $driver = shift;
450    my($class, $terms, $args) = @_;
451
452    my $stmt = $args->{sql_statement} || Data::ObjectDriver::SQL->new;
453
454    if (my $tbl = $class->datasource) {
455        my $cols = $class->column_names;
456        my $dbd = $driver->dbd;
457        my %fetch = $args->{fetchonly} ?
458            (map { $_ => 1 } @{ $args->{fetchonly} }) : ();
459        for my $col (@$cols) {
460            if (keys %fetch) {
461                next unless $fetch{$col};
462            }
463            my $dbcol = join '.', $tbl, $dbd->db_column_name($tbl, $col);
464            $stmt->add_select($dbcol => $col);
465        }
466
467        $stmt->from([ $tbl ]);
468
469        if (defined($terms)) {
470            for my $col (keys %$terms) {
471                my $db_col = $dbd->db_column_name($tbl, $col);
472                $stmt->add_where(join('.', $tbl, $db_col), $terms->{$col});
473            }
474        }
475    }
476    $stmt->limit($args->{limit}) if $args->{limit};
477    $stmt->offset($args->{offset}) if $args->{offset};
478    if ($args->{sort} || $args->{direction}) {
479        my $order = $args->{sort} || 'id';
480        my $dir = $args->{direction} &&
481                  $args->{direction} eq 'descend' ? 'DESC' : 'ASC';
482        $stmt->order({
483            column => $order,
484            desc   => $dir,
485        });
486    }
487    $stmt;
488}
489
4901;
Note: See TracBrowser for help on using the browser.