Commit | Line | Data |
18360aed |
1 | package DBIx::Class::Storage::DBI::Oracle::Generic; |
2 | |
3 | use strict; |
4 | use warnings; |
5 | |
7137528d |
6 | =head1 NAME |
7 | |
7a84c41b |
8 | DBIx::Class::Storage::DBI::Oracle::Generic - Oracle Support for DBIx::Class |
7137528d |
9 | |
10 | =head1 SYNOPSIS |
11 | |
12 | # In your table classes |
13 | __PACKAGE__->load_components(qw/PK::Auto Core/); |
2e46b6eb |
14 | __PACKAGE__->add_columns({ id => { sequence => 'mysequence', auto_nextval => 1 } }); |
7137528d |
15 | __PACKAGE__->set_primary_key('id'); |
16 | __PACKAGE__->sequence('mysequence'); |
17 | |
c0024355 |
18 | # Somewhere in your Code |
19 | # add some data to a table with a hierarchical relationship |
20 | $schema->resultset('Person')->create ({ |
21 | firstname => 'foo', |
22 | lastname => 'bar', |
23 | children => [ |
24 | { |
25 | firstname => 'child1', |
26 | lastname => 'bar', |
27 | children => [ |
28 | { |
29 | firstname => 'grandchild', |
30 | lastname => 'bar', |
31 | } |
32 | ], |
33 | }, |
34 | { |
35 | firstname => 'child2', |
36 | lastname => 'bar', |
37 | }, |
38 | ], |
39 | }); |
40 | |
41 | # select from the hierarchical relationship |
42 | my $rs = $schema->resultset('Person')->search({}, |
43 | { |
44 | 'start_with' => { 'firstname' => 'foo', 'lastname' => 'bar' }, |
45 | 'connect_by' => { 'parentid' => 'prior persionid'}, |
46 | 'order_siblings_by' => 'firstname ASC', |
47 | }; |
48 | ); |
49 | |
50 | # this will select the whole tree starting from person "foo bar", creating |
51 | # following query: |
52 | # SELECT |
53 | # me.persionid me.firstname, me.lastname, me.parentid |
54 | # FROM |
55 | # person me |
56 | # START WITH |
57 | # firstname = 'foo' and lastname = 'bar' |
58 | # CONNECT BY |
59 | # parentid = prior persionid |
60 | # ORDER SIBLINGS BY |
61 | # firstname ASC |
62 | |
7137528d |
63 | =head1 DESCRIPTION |
64 | |
c0024355 |
65 | This class implements autoincrements for Oracle and adds support for Oracle |
66 | specific hierarchical queries. |
7137528d |
67 | |
68 | =head1 METHODS |
69 | |
70 | =cut |
71 | |
db56cf3d |
72 | use base qw/DBIx::Class::Storage::DBI/; |
2ad62d97 |
73 | use mro 'c3'; |
18360aed |
74 | |
c0024355 |
75 | __PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::Oracle'); |
76 | |
18360aed |
77 | sub _dbh_last_insert_id { |
2e46b6eb |
78 | my ($self, $dbh, $source, @columns) = @_; |
79 | my @ids = (); |
80 | foreach my $col (@columns) { |
81 | my $seq = ($source->column_info($col)->{sequence} ||= $self->get_autoinc_seq($source,$col)); |
82 | my $id = $self->_sequence_fetch( 'currval', $seq ); |
83 | push @ids, $id; |
84 | } |
85 | return @ids; |
18360aed |
86 | } |
87 | |
88 | sub _dbh_get_autoinc_seq { |
89 | my ($self, $dbh, $source, $col) = @_; |
90 | |
91 | # look up the correct sequence automatically |
92 | my $sql = q{ |
93 | SELECT trigger_body FROM ALL_TRIGGERS t |
94 | WHERE t.table_name = ? |
95 | AND t.triggering_event = 'INSERT' |
96 | AND t.status = 'ENABLED' |
97 | }; |
98 | |
99 | # trigger_body is a LONG |
7a84c41b |
100 | local $dbh->{LongReadLen} = 64 * 1024 if ($dbh->{LongReadLen} < 64 * 1024); |
18360aed |
101 | |
cb464582 |
102 | my $sth; |
103 | |
e6dd7b42 |
104 | my $source_name; |
105 | if ( ref $source->name ne 'SCALAR' ) { |
106 | $source_name = $source->name; |
107 | } |
108 | else { |
109 | $source_name = ${$source->name}; |
110 | } |
111 | |
cb464582 |
112 | # check for fully-qualified name (eg. SCHEMA.TABLENAME) |
e6dd7b42 |
113 | if ( my ( $schema, $table ) = $source_name =~ /(\w+)\.(\w+)/ ) { |
cb464582 |
114 | $sql = q{ |
115 | SELECT trigger_body FROM ALL_TRIGGERS t |
116 | WHERE t.owner = ? AND t.table_name = ? |
117 | AND t.triggering_event = 'INSERT' |
118 | AND t.status = 'ENABLED' |
119 | }; |
120 | $sth = $dbh->prepare($sql); |
121 | $sth->execute( uc($schema), uc($table) ); |
122 | } |
123 | else { |
124 | $sth = $dbh->prepare($sql); |
e6dd7b42 |
125 | $sth->execute( uc( $source_name ) ); |
cb464582 |
126 | } |
18360aed |
127 | while (my ($insert_trigger) = $sth->fetchrow_array) { |
128 | return uc($1) if $insert_trigger =~ m!(\w+)\.nextval!i; # col name goes here??? |
129 | } |
66cab05c |
130 | $self->throw_exception("Unable to find a sequence INSERT trigger on table '" . $source->name . "'."); |
18360aed |
131 | } |
132 | |
2e46b6eb |
133 | sub _sequence_fetch { |
134 | my ( $self, $type, $seq ) = @_; |
9ae966b9 |
135 | my ($id) = $self->_get_dbh->selectrow_array("SELECT ${seq}.${type} FROM DUAL"); |
2e46b6eb |
136 | return $id; |
137 | } |
138 | |
6dc4be0f |
139 | sub _ping { |
c2481821 |
140 | my $self = shift; |
7ba7a57d |
141 | |
6dc4be0f |
142 | my $dbh = $self->_dbh or return 0; |
7ba7a57d |
143 | |
6dc4be0f |
144 | local $dbh->{RaiseError} = 1; |
c2d7baef |
145 | |
6dc4be0f |
146 | eval { |
147 | $dbh->do("select 1 from dual"); |
148 | }; |
7ba7a57d |
149 | |
6dc4be0f |
150 | return $@ ? 0 : 1; |
c2481821 |
151 | } |
152 | |
d789fa99 |
153 | sub _dbh_execute { |
154 | my $self = shift; |
155 | my ($dbh, $op, $extra_bind, $ident, $bind_attributes, @args) = @_; |
156 | |
157 | my $wantarray = wantarray; |
d789fa99 |
158 | |
c2d7baef |
159 | my (@res, $exception, $retried); |
160 | |
0f0abc97 |
161 | RETRY: { |
162 | do { |
163 | eval { |
164 | if ($wantarray) { |
c3515436 |
165 | @res = $self->next::method(@_); |
0f0abc97 |
166 | } else { |
c3515436 |
167 | $res[0] = $self->next::method(@_); |
0f0abc97 |
168 | } |
169 | }; |
170 | $exception = $@; |
171 | if ($exception =~ /ORA-01003/) { |
172 | # ORA-01003: no statement parsed (someone changed the table somehow, |
173 | # invalidating your cursor.) |
174 | my ($sql, $bind) = $self->_prep_for_execute($op, $extra_bind, $ident, \@args); |
175 | delete $dbh->{CachedKids}{$sql}; |
d789fa99 |
176 | } else { |
0f0abc97 |
177 | last RETRY; |
d789fa99 |
178 | } |
0f0abc97 |
179 | } while (not $retried++); |
180 | } |
d789fa99 |
181 | |
182 | $self->throw_exception($exception) if $exception; |
183 | |
184 | wantarray ? @res : $res[0] |
185 | } |
186 | |
7137528d |
187 | =head2 get_autoinc_seq |
188 | |
189 | Returns the sequence name for an autoincrement column |
190 | |
191 | =cut |
192 | |
18360aed |
193 | sub get_autoinc_seq { |
194 | my ($self, $source, $col) = @_; |
d4daee7b |
195 | |
373940e1 |
196 | $self->dbh_do('_dbh_get_autoinc_seq', $source, $col); |
18360aed |
197 | } |
198 | |
7137528d |
199 | =head2 columns_info_for |
200 | |
201 | This wraps the superclass version of this method to force table |
202 | names to uppercase |
203 | |
204 | =cut |
205 | |
18360aed |
206 | sub columns_info_for { |
207 | my ($self, $table) = @_; |
208 | |
209 | $self->next::method(uc($table)); |
210 | } |
211 | |
8f7e044c |
212 | =head2 datetime_parser_type |
213 | |
214 | This sets the proper DateTime::Format module for use with |
215 | L<DBIx::Class::InflateColumn::DateTime>. |
216 | |
217 | =cut |
218 | |
219 | sub datetime_parser_type { return "DateTime::Format::Oracle"; } |
220 | |
9900b569 |
221 | =head2 connect_call_datetime_setup |
d2a3958e |
222 | |
223 | Used as: |
224 | |
9900b569 |
225 | on_connect_call => 'datetime_setup' |
d2a3958e |
226 | |
82f6f45f |
227 | In L<DBIx::Class::Storage::DBI/connect_info> to set the session nls date, and |
228 | timestamp values for use with L<DBIx::Class::InflateColumn::DateTime> and the |
229 | necessary environment variables for L<DateTime::Format::Oracle>, which is used |
230 | by it. |
d2a3958e |
231 | |
82f6f45f |
232 | Maximum allowable precision is used, unless the environment variables have |
233 | already been set. |
d2a3958e |
234 | |
9900b569 |
235 | These are the defaults used: |
236 | |
237 | $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS'; |
238 | $ENV{NLS_TIMESTAMP_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF'; |
239 | $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM'; |
240 | |
d9e53b85 |
241 | To get more than second precision with L<DBIx::Class::InflateColumn::DateTime> |
242 | for your timestamps, use something like this: |
243 | |
244 | use Time::HiRes 'time'; |
245 | my $ts = DateTime->from_epoch(epoch => time); |
246 | |
d2a3958e |
247 | =cut |
248 | |
9900b569 |
249 | sub connect_call_datetime_setup { |
d2a3958e |
250 | my $self = shift; |
d2a3958e |
251 | |
252 | my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS'; |
253 | my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||= |
254 | 'YYYY-MM-DD HH24:MI:SS.FF'; |
255 | my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= |
256 | 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM'; |
257 | |
7a84c41b |
258 | $self->_do_query("alter session set nls_date_format = '$date_format'"); |
259 | $self->_do_query( |
260 | "alter session set nls_timestamp_format = '$timestamp_format'"); |
261 | $self->_do_query( |
262 | "alter session set nls_timestamp_tz_format='$timestamp_tz_format'"); |
d2a3958e |
263 | } |
264 | |
5db2758d |
265 | =head2 source_bind_attributes |
266 | |
267 | Handle LOB types in Oracle. Under a certain size (4k?), you can get away |
268 | with the driver assuming your input is the deprecated LONG type if you |
269 | encode it as a hex string. That ain't gonna fly at larger values, where |
270 | you'll discover you have to do what this does. |
271 | |
272 | This method had to be overridden because we need to set ora_field to the |
273 | actual column, and that isn't passed to the call (provided by Storage) to |
274 | bind_attribute_by_data_type. |
275 | |
276 | According to L<DBD::Oracle>, the ora_field isn't always necessary, but |
277 | adding it doesn't hurt, and will save your bacon if you're modifying a |
278 | table with more than one LOB column. |
279 | |
280 | =cut |
281 | |
e6dd7b42 |
282 | sub source_bind_attributes |
5db2758d |
283 | { |
efc5bf40 |
284 | require DBD::Oracle; |
5db2758d |
285 | my $self = shift; |
286 | my($source) = @_; |
287 | |
288 | my %bind_attributes; |
289 | |
290 | foreach my $column ($source->columns) { |
291 | my $data_type = $source->column_info($column)->{data_type} || ''; |
292 | next unless $data_type; |
293 | |
294 | my %column_bind_attrs = $self->bind_attribute_by_data_type($data_type); |
295 | |
296 | if ($data_type =~ /^[BC]LOB$/i) { |
0d1207e8 |
297 | $column_bind_attrs{'ora_type'} = uc($data_type) eq 'CLOB' ? |
298 | DBD::Oracle::ORA_CLOB() : |
299 | DBD::Oracle::ORA_BLOB(); |
5db2758d |
300 | $column_bind_attrs{'ora_field'} = $column; |
301 | } |
302 | |
303 | $bind_attributes{$column} = \%column_bind_attrs; |
304 | } |
305 | |
306 | return \%bind_attributes; |
307 | } |
308 | |
1816be4f |
309 | sub _svp_begin { |
310 | my ($self, $name) = @_; |
311 | |
e33b954c |
312 | $self->_get_dbh->do("SAVEPOINT $name"); |
1816be4f |
313 | } |
314 | |
281719d2 |
315 | # Oracle automatically releases a savepoint when you start another one with the |
316 | # same name. |
317 | sub _svp_release { 1 } |
318 | |
319 | sub _svp_rollback { |
320 | my ($self, $name) = @_; |
321 | |
9ae966b9 |
322 | $self->_get_dbh->do("ROLLBACK TO SAVEPOINT $name") |
281719d2 |
323 | } |
324 | |
c0024355 |
325 | sub _select_args { |
326 | my ($self, $ident, $select, $where, $attrs) = @_; |
327 | |
328 | my $connect_by_args = {}; |
329 | if ( $attrs->{connect_by} || $attrs->{start_with} || $attrs->{order_siblings_by} ) { |
330 | $connect_by_args = { |
331 | connect_by => $attrs->{connect_by}, |
332 | start_with => $attrs->{start_with}, |
333 | order_siblings_by => $attrs->{order_siblings_by}, |
334 | } |
335 | } |
336 | |
337 | my @rv = $self->next::method($ident, $select, $where, $attrs); |
338 | |
339 | return (@rv, $connect_by_args); |
340 | } |
341 | |
342 | =head1 ATTRIBUTES |
343 | |
344 | Following additional attributes can be used in resultsets. |
345 | |
346 | =head2 connect_by |
347 | |
348 | =over 4 |
349 | |
350 | =item Value: \%connect_by |
351 | |
352 | =back |
353 | |
354 | A hashref of conditions used to specify the relationship between parent rows |
355 | and child rows of the hierarchy. |
356 | |
357 | connect_by => { parentid => 'prior personid' } |
358 | |
359 | # adds a connect by statement to the query: |
360 | # SELECT |
361 | # me.persionid me.firstname, me.lastname, me.parentid |
362 | # FROM |
363 | # person me |
364 | # CONNECT BY |
365 | # parentid = prior persionid |
366 | |
367 | =head2 start_with |
368 | |
369 | =over 4 |
370 | |
371 | =item Value: \%condition |
372 | |
373 | =back |
374 | |
375 | A hashref of conditions which specify the root row(s) of the hierarchy. |
376 | |
377 | It uses the same syntax as L<DBIx::Class::ResultSet/search> |
378 | |
379 | start_with => { firstname => 'Foo', lastname => 'Bar' } |
380 | |
381 | # SELECT |
382 | # me.persionid me.firstname, me.lastname, me.parentid |
383 | # FROM |
384 | # person me |
385 | # START WITH |
386 | # firstname = 'foo' and lastname = 'bar' |
387 | # CONNECT BY |
388 | # parentid = prior persionid |
389 | |
390 | =head2 order_siblings_by |
391 | |
392 | =over 4 |
393 | |
394 | =item Value: ($order_siblings_by | \@order_siblings_by) |
395 | |
396 | =back |
397 | |
398 | Which column(s) to order the siblings by. |
399 | |
400 | It uses the same syntax as L<DBIx::Class::ResultSet/order_by> |
401 | |
402 | 'order_siblings_by' => 'firstname ASC' |
403 | |
404 | # SELECT |
405 | # me.persionid me.firstname, me.lastname, me.parentid |
406 | # FROM |
407 | # person me |
408 | # CONNECT BY |
409 | # parentid = prior persionid |
410 | # ORDER SIBLINGS BY |
411 | # firstname ASC |
412 | |
7a84c41b |
413 | =head1 AUTHOR |
18360aed |
414 | |
7a84c41b |
415 | See L<DBIx::Class/CONTRIBUTORS>. |
18360aed |
416 | |
417 | =head1 LICENSE |
418 | |
419 | You may distribute this code under the same terms as Perl itself. |
420 | |
421 | =cut |
7137528d |
422 | |
423 | 1; |