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

Revision 7, 10.7 kB (checked in by sky, 4 years ago)

r10@crucially-3 (orig r927): btrott | 2005-06-23 15:56:28 -0700
Changed to assume use of SERIAL columns, which auto-generate IDs for us

  • 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;
10
11__PACKAGE__->mk_accessors(qw( dsn username password dbh ));
12
13# set to 1 during development to get sql statements in the error log
14use constant SQLDEBUG => 0;
15
16sub init {
17    my $driver = shift;
18    my %param = @_;
19    for my $key (keys %param) {
20        $driver->$key($param{$key});
21    }
22    ## Rebless the driver into the DSN-specific subclass (e.g. "mysql").
23    my($type) = lc($driver->dsn) =~ /^dbi:(\w*)/;
24    my $class = ref($driver) . '::' . $type;
25    eval "use $class";
26    die $@ if $@;
27    bless $driver, $class;
28    $driver;
29}
30
31sub generate_pk {
32    my $driver = shift;
33    if (my $generator = $driver->pk_generator) {
34        return $generator->(@_);
35    }
36}
37sub fetch_id { undef }
38sub offset_implemented { 1 }
39
40sub db_column_name {
41    my ($driver, $table, $column) = @_; 
42    return $column;
43}
44
45# Override in DB Driver to pass correct attributes to bind_param call
46sub bind_param_attributes { return undef }
47
48sub init_db {
49    my $driver = shift;
50    my $dbh;
51    eval {
52        local $SIG{ALRM} = sub { die "alarm\n" };
53        $dbh = DBI->connect($driver->dsn, $driver->username, $driver->password,
54            { RaiseError => 1, PrintError => 0, AutoCommit => 1 })
55            or Carp::croak("Connection error: " . $DBI::errstr);
56        alarm 0;
57    };
58    if ($@) {
59        Carp::croak(@$ eq "alarm\n" ? "Connection timeout" : $@);
60    }
61    $dbh;
62}
63
64sub rw_handle {
65    my $driver = shift;
66    my $db = shift || 'main';
67    my $dbh = $driver->dbh;
68    unless ($dbh) {
69        $dbh = $driver->init_db($db) or die $driver->errstr;
70        $driver->dbh($dbh);
71    }
72    $dbh;
73}
74*r_handle = \&rw_handle;
75
76sub search {
77    my $driver = shift;
78    my($class, $terms, $args) = @_;
79
80    my $stmt = $driver->prepare_statement($class, $terms, $args);
81    my $tbl = $class->datasource;
82    my(%rec, @bind, @cols);
83    my $cols = $class->column_names;
84
85    my $primary_key = $class->properties->{primary_key};
86    for my $col (@$cols) {
87        if ($args->{fetchonly}) {
88            next unless $args->{fetchonly}{$col};
89        }
90        my $dbcol  = $driver->db_column_name($tbl, $col);
91        push @cols, $dbcol;
92        push @bind, \$rec{$col};
93    }
94    my $tmp = "SELECT ";
95    $tmp .= "DISTINCT " if $args->{join} && $args->{join}[3]{unique};
96   
97    $tmp .= join(', ', @cols) . "\n";
98    my $sql = $tmp . $stmt->as_sql;
99    my $dbh = $driver->r_handle($class->properties->{db});
100    warn $sql if (SQLDEBUG);
101    my $sth = $dbh->prepare_cached($sql);
102    $sth->execute(@{ $stmt->{bind} });
103    $sth->bind_columns(undef, @bind);
104
105    # need to slurp 'offset' rows for DBs that cannot do it themselves
106    if (!$driver->offset_implemented && $args->{offset}) {
107        for (1..$args->{offset}) {
108            $sth->fetch;
109        }
110    }
111
112    my $iter = sub {
113        unless ($sth->fetch) {
114            $sth->finish;
115            return;
116        }
117        my $obj;
118        $obj = $class->new;
119        $obj->set_values(\%rec);
120        $obj->is_loaded(1);
121        $obj;
122    };
123   
124    if (wantarray) {
125        my @objs;
126        while (my $obj = $iter->()) {
127            push @objs, $obj;
128        }
129        return @objs;
130    } else {
131        return $iter;
132    }
133}
134
135sub primary_key_to_terms {
136    my $driver = shift;
137    my($class, $id) = @_;
138    my $pk = $class->properties->{primary_key};
139    $pk = [ $pk ] unless ref($pk) eq 'ARRAY';
140    $id = [ $id ] unless ref($id) eq 'ARRAY';
141    my $i = 0;
142    my %terms;
143    @terms{@$pk} = @$id;
144    \%terms;
145}
146
147sub lookup {
148    my $driver = shift;
149    my($class, $id) = @_;
150    my @obj = $driver->search($class,
151        $driver->primary_key_to_terms($class, $id), { limit => 1 });
152    $obj[0];
153}
154
155sub select_one {
156    my $driver = shift;
157    my($dbh, $sql, $bind) = @_;
158    my $sth = $dbh->prepare_cached($sql);
159    $sth->execute(@$bind);
160    $sth->bind_columns(undef, \my($val));
161    $sth->fetch or return;
162    $sth->finish;
163    $val;
164}
165
166sub count {
167    my $driver = shift;
168    my($class, $terms, $args) = @_;
169    my $stmt = $driver->prepare_statement($class, $terms, $args);
170    ## Remove any order by clauses, because they will cause errors in
171    ## some drivers (and they're not necessary)
172    delete $stmt->{order};
173    my $sql = "SELECT COUNT(*)\n" . $stmt->as_sql;
174    warn $sql if (SQLDEBUG);
175    my $count = $driver->select_one(
176        $driver->r_handle($class->properties->{db}), $sql, $stmt->{bind}
177    );
178    $count;
179}
180
181sub data_exists {
182    my $driver = shift;
183    my($class, $terms, $args) = @_;
184
185    # add a limit 1 to select only one row
186    $args ||= {};
187    $args->{limit} = 1;
188
189    my $stmt = $driver->prepare_statement($class, $terms, $args);
190    ## Remove any order by clauses, because they will cause errors in
191    ## some drivers (and they're not necessary)
192    delete $stmt->{order};
193    my $sql = "SELECT 1\n" . $stmt->as_sql;
194    warn $sql if (SQLDEBUG);
195    my $exists = $driver->select_one(
196        $driver->r_handle($class->properties->{db}), $sql, $stmt->{bind}
197    );
198    $exists;
199}
200
201sub min {
202    my $driver = shift;
203    my($class, $terms, $args) = @_;
204    my $stmt = $driver->prepare_statement($class, $terms, $args);
205    ## Remove any order by clauses, because they will cause errors in
206    ## some drivers (and they're not necessary)
207    delete $stmt->{order};
208    my $field = $class->datasource . '_' . $args->{min_col};
209    my $sql = "SELECT MIN($field)\n" . $stmt->as_sql;
210    warn $sql if (SQLDEBUG);
211    my $min = $driver->select_one(
212        $driver->r_handle($class->properties->{db}), $sql, $stmt->{bind}
213    );
214    $min || undef;
215}
216
217sub sum {
218    my $driver = shift;
219    my($class, $terms, $args) = @_;
220    my $stmt = $driver->prepare_statement($class, $terms, $args);
221    ## Remove any order by clauses, because they will cause errors in
222    ## some drivers (and they're not necessary)
223    delete $stmt->{order};
224    my $field = $class->datasource . '_' . $args->{sum_col};
225    my $sql = "SELECT SUM($field)\n" . $stmt->as_sql;
226    warn $sql if (SQLDEBUG);
227    my $sum = $driver->select_one(
228        $driver->r_handle($class->properties->{db}), $sql, $stmt->{bind}
229    );
230    $sum || 0;
231}
232
233sub exists {
234    my $driver = shift;
235    my($obj) = @_;
236    return unless $obj->id;
237    my $tbl = $obj->datasource;
238    my $sql = "SELECT 1 FROM $tbl WHERE id = ?";
239    my $dbh = $driver->r_handle($obj->properties->{db});
240    warn $sql if (SQLDEBUG);
241    my $sth = $dbh->prepare_cached($sql) or return;
242    $sth->execute($obj->id) or return;
243    my $exists = $sth->fetch;
244    $sth->finish;
245    $exists;
246}
247
248sub insert {
249    my $driver = shift;
250    my($obj) = @_;
251    my $cols = $obj->column_names;
252    unless ($obj->has_primary_key) {
253        ## If we don't already have a primary key assigned for this object, we
254        ## may need to generate one (depending on the underlying DB
255        ## driver). If the driver gives us a new ID, we insert that into
256        ## the new record; otherwise, we assume that the DB is using an
257        ## auto-increment column of some sort, so we don't specify an ID
258        ## at all.
259        my $generated = $driver->generate_pk($obj);
260        unless ($generated) {
261            my $pk = $obj->properties->{primary_key};
262            $pk = [ $pk ] unless ref($pk) eq 'ARRAY';
263            my %pk = map { $_ => 1 } @$pk;
264            $cols = [ grep !$pk{$_} || defined $obj->$_(), @$cols ];
265        }
266    }
267    my $tbl = $obj->datasource;
268    my $sql = "INSERT INTO $tbl\n";
269    $sql .= '(' . join(', ', map $driver->db_column_name($tbl, $_), @$cols) . ')' . "\n" .
270            'VALUES (' . join(', ', ('?') x @$cols) . ')' . "\n";
271    my $dbh = $driver->rw_handle($obj->properties->{db});
272    warn $sql if (SQLDEBUG);
273    my $sth = $dbh->prepare_cached($sql);
274    my $i = 1;
275    my $col_defs = $obj->properties->{column_defs};
276    for my $col (@$cols) {
277        my $val = $obj->column($col);
278        my $type = $col_defs->{$col} || 'char';
279        my $attr = $driver->bind_param_attributes($type);
280        $sth->bind_param($i++, $val, $attr);
281    }
282    $sth->execute;
283    $sth->finish;
284
285    ## Now, if we didn't have an object ID, we need to grab the
286    ## newly-assigned ID.
287    unless ($obj->has_primary_key) {
288        $obj->id($driver->fetch_id($dbh, $sth));
289    }
290    1;
291}
292
293sub update {
294    my $driver = shift;
295    my($obj) = @_;
296    my $cols = $obj->column_names;
297    my $pk = $obj->properties->{primary_key};
298    $pk = [ $pk ] unless ref($pk) eq 'ARRAY';
299    my %pk = map { $_ => 1 } @$pk;
300    $cols = [ grep !$pk{$_}, @$cols ];
301    my $tbl = $obj->datasource;
302    my $sql = "UPDATE $tbl SET\n";
303    $sql .= join(', ', map $driver->db_column_name($tbl, $_) . " = ?", @$cols) . "\n";
304    my $stmt = $driver->prepare_statement(ref($obj), $obj->primary_key);
305    $sql .= $stmt->as_sql_where;
306   
307    my $dbh = $driver->rw_handle($obj->properties->{db});
308    warn $sql if (SQLDEBUG);
309    my $sth = $dbh->prepare_cached($sql);
310    my $i = 1;
311    my $col_defs = $obj->properties->{column_defs};
312    for my $col (@$cols) {
313        my $val = $obj->column($col);
314        my $type = $col_defs->{$col} || 'char';
315        my $attr = $driver->bind_param_attributes($type);
316        $sth->bind_param($i++, $val, $attr);
317    }
318
319    ## Bind the primary key value(s).
320    for my $val (@{ $stmt->{bind} }) {
321        $sth->bind_param($i++, $val);
322    }
323
324    $sth->execute;
325    $sth->finish;
326    1;
327}
328
329sub remove {
330    my $driver = shift;
331    my($obj) = @_;
332    return unless $obj->has_primary_key;
333    my $tbl = $obj->datasource;
334    my $sql = "DELETE FROM $tbl\n";
335    my $stmt = $driver->prepare_statement(ref($obj),
336        $driver->primary_key_to_terms(ref($obj), $obj->primary_key));
337    $sql .= $stmt->as_sql_where;
338    my $dbh = $driver->rw_handle($obj->properties->{db});
339    warn $sql if (SQLDEBUG);
340    my $sth = $dbh->prepare_cached($sql);
341    $sth->execute(@{ $stmt->{bind} });
342    $sth->finish;
343    1;
344}
345
346sub commit {
347    my $driver = shift;
348    if (my $dbh = $driver->dbh) {
349        $dbh->commit;
350    }
351    1;
352}
353
354sub rollback {
355    my $driver = shift;
356    if (my $dbh = $driver->dbh) {
357        $dbh->rollback;
358    }
359    1;
360}
361
362sub DESTROY {
363    if (my $dbh = shift->dbh) {
364        $dbh->disconnect if $dbh;
365    }
366}
367
368sub prepare_statement {
369    my $driver = shift;
370    my($class, $terms, $args) = @_;
371    my $stmt = Data::ObjectDriver::SQL->new;
372    $stmt->from([ $class->datasource ]);
373    if (defined($terms)) {
374        for my $col (keys %$terms) {
375            $stmt->add_where($col, $terms->{$col});
376        }
377    }
378    $stmt->limit($args->{limit});
379    $stmt->offset($args->{offset});
380    if ($args->{sort} || $args->{direction}) {
381        my $order = $args->{sort} || 'id';
382        my $dir = $args->{direction} &&
383                  $args->{direction} eq 'descend' ? 'DESC' : 'ASC';
384        $stmt->order({
385            column => $order,
386            desc   => $dir,
387        });
388    }
389    $stmt;
390}
391
3921;
Note: See TracBrowser for help on using the browser.