Revision 660c7e53
Von Sven Schöling vor fast 8 Jahren hinzugefügt
SL/DB.pm | ||
---|---|---|
126 | 126 |
my ($self, $code, @args) = @_; |
127 | 127 |
|
128 | 128 |
return $code->(@args) if $self->in_transaction; |
129 |
if (wantarray) { |
|
130 |
my @result; |
|
131 |
return $self->do_transaction(sub { @result = $code->(@args) }) ? @result : (); |
|
132 | 129 |
|
133 |
} else { |
|
134 |
my $result; |
|
135 |
return $self->do_transaction(sub { $result = $code->(@args) }) ? $result : undef; |
|
136 |
} |
|
130 |
my (@result, $result); |
|
131 |
my $rv = 1; |
|
132 |
|
|
133 |
local $@; |
|
134 |
|
|
135 |
eval { |
|
136 |
wantarray |
|
137 |
? $self->do_transaction(sub { @result = $code->(@args) }) |
|
138 |
: $self->do_transaction(sub { $result = $code->(@args) }); |
|
139 |
} or do { |
|
140 |
my $error = $self->error; |
|
141 |
if (ref $error) { |
|
142 |
if ($error->isa('SL::X::DBError')) { |
|
143 |
# gobble the exception |
|
144 |
} else { |
|
145 |
$error->rethrow; |
|
146 |
} |
|
147 |
} else { |
|
148 |
die $self->error; |
|
149 |
} |
|
150 |
}; |
|
151 |
|
|
152 |
return wantarray ? @result : $result; |
|
137 | 153 |
} |
138 | 154 |
|
139 | 155 |
1; |
... | ... | |
173 | 189 |
# do stuff with $self |
174 | 190 |
}); |
175 | 191 |
|
176 |
There are two big differences between C<with_transaction> and |
|
177 |
L<Rose::DB/do_transaction>: the handling of an already-running |
|
178 |
transaction and the handling of return values. |
|
192 |
This is a wrapper around L<Rose::DB/do_transaction> that does a few additional |
|
193 |
things, and should always be used in favour of the other: |
|
179 | 194 |
|
180 |
The first difference revolves around when a transaction is started and |
|
181 |
committed/rolled back. Rose's C<do_transaction> will always start one, |
|
182 |
then execute the code reference and commit afterwards (or roll back if |
|
183 |
an exception occurs). |
|
195 |
=over 4 |
|
184 | 196 |
|
185 |
This prevents the caller from combining several pieces of code using |
|
186 |
C<do_transaction> reliably as results committed by an inner |
|
187 |
transaction will be permanent even if the outer transaction is rolled |
|
188 |
back. |
|
197 |
=item Composition of transactions |
|
189 | 198 |
|
190 |
Therefore our C<with_transaction> works differently: it will only |
|
191 |
start a transaction if no transaction is currently active on the |
|
192 |
database connection. |
|
199 |
When C<with_transaction> is called without a running transaction, a new one is |
|
200 |
created. If it is called within a running transaction, it performs no |
|
201 |
additional handling. This means that C<with_transaction> can be safely used |
|
202 |
within another C<with_transaction>, whereas L<Rose::DB/do_transaction> can not. |
|
193 | 203 |
|
194 |
The second big difference to L<Rose::DB/do_transaction> is the |
|
195 |
handling of returned values. Basically our C<with_transaction> will |
|
196 |
return the values that the code reference C<$code_ref> returns (or |
|
197 |
C<undef> if the transaction was rolled back). Rose's C<do_transaction> |
|
198 |
on the other hand will only return a value signaling the transaction's |
|
199 |
status. |
|
204 |
=item Return values |
|
200 | 205 |
|
201 |
In more detail: |
|
206 |
C<with_transaction> adopts the behaviour of C<eval> in that it returns the |
|
207 |
result of the inner block, and C<undef> if an error occured. This way you can |
|
208 |
use the same pattern you would normally use with C<eval> for |
|
209 |
C<with_transaction>: |
|
202 | 210 |
|
203 |
=over 2 |
|
211 |
SL::DB->client->with_transaction(sub { |
|
212 |
# do stuff |
|
213 |
# and return nominal true value |
|
214 |
1; |
|
215 |
}) or do { |
|
216 |
# transaction error handling |
|
217 |
my $error = SL::DB->client->error; |
|
218 |
} |
|
204 | 219 |
|
205 |
=item * If a transaction is already active then C<with_transaction> |
|
206 |
will simply return the result of calling C<$code_ref> as-is preserving |
|
207 |
context. |
|
220 |
or you can use it to safely calulate things. |
|
208 | 221 |
|
209 |
=item * If no transaction is started then C<$code_ref> will be wrapped |
|
210 |
in one. C<with_transaction>'s return value depends on the result of |
|
211 |
that transaction. If the it succeeds then the return value of |
|
212 |
C<$code_ref> will be returned preserving context. Otherwise C<undef> |
|
213 |
will be returned in scalar context and an empty list in list context. |
|
222 |
=item Error handling |
|
214 | 223 |
|
215 |
=back |
|
224 |
The original L<Rose::DB/do_transaction> gobbles up all execptions and expects |
|
225 |
the caller to manually check return value and error, and then to process all |
|
226 |
exceptions as strings. This is very fragile and generally a step backwards from |
|
227 |
proper exception handling. |
|
228 |
|
|
229 |
C<with_transaction> only gobbles up exception that are used to signal an |
|
230 |
error in the transaction, and returns undef on those. All other exceptions |
|
231 |
bubble out of the transaction like normal, so that it is transparent to typoes, |
|
232 |
runtime exceptions and other generally wanted things. |
|
233 |
|
|
234 |
If you just use the snippet above, your code will catch everything related to |
|
235 |
the transaction aborting, but will not catch other errors that might have been |
|
236 |
thrown. The transaction will be rollbacked in both cases. |
|
216 | 237 |
|
217 |
So if you want to differentiate between "transaction failed" and |
|
218 |
"succeeded" then your C<$code_ref> should never return C<undef> |
|
219 |
itself. |
|
238 |
If you want to play nice in case your transaction is embedded in another |
|
239 |
transaction, just rethrow the error: |
|
240 |
|
|
241 |
$db->with_transaction(sub { |
|
242 |
# code deep in the engine |
|
243 |
1; |
|
244 |
}) or die $db->error; |
|
245 |
|
|
246 |
=back |
|
220 | 247 |
|
221 | 248 |
=back |
222 | 249 |
|
SL/DB/Helper/Metadata.pm | ||
---|---|---|
1 | 1 |
package SL::DB::Helper::Metadata; |
2 | 2 |
|
3 | 3 |
use strict; |
4 |
use SL::X; |
|
4 | 5 |
|
5 | 6 |
use Rose::DB::Object::Metadata; |
6 | 7 |
use SL::DB::Helper::ConventionManager; |
... | ... | |
31 | 32 |
SL::DB::Helper::Attr::auto_make($self->class); |
32 | 33 |
} |
33 | 34 |
|
35 |
sub handle_error { |
|
36 |
my($self, $object) = @_; |
|
37 |
|
|
38 |
# these are used as Rose internal canaries, don't wrap them |
|
39 |
die $object->error if UNIVERSAL::isa($object->error, 'Rose::DB::Object::Exception'); |
|
40 |
|
|
41 |
die SL::X::DBRoseError->new( |
|
42 |
error => $object->error, |
|
43 |
class => ref($object), |
|
44 |
metaobject => $self, |
|
45 |
object => $object, |
|
46 |
); |
|
47 |
} |
|
48 |
|
|
34 | 49 |
1; |
SL/DB/Object.pm | ||
---|---|---|
148 | 148 |
SL::DB::Object::Hooks::run_hooks($self, 'after_save', $result); |
149 | 149 |
|
150 | 150 |
1; |
151 |
}) || die $self->error; |
|
151 |
}) || die $self->db->error;
|
|
152 | 152 |
|
153 | 153 |
return $result; |
154 | 154 |
} |
... | ... | |
164 | 164 |
SL::DB::Object::Hooks::run_hooks($self, 'after_delete', $result); |
165 | 165 |
|
166 | 166 |
1; |
167 |
}) || die $self->error; |
|
167 |
}) || die $self->db->error;
|
|
168 | 168 |
|
169 | 169 |
return $result; |
170 | 170 |
} |
SL/Form.pm | ||
---|---|---|
344 | 344 |
} |
345 | 345 |
|
346 | 346 |
sub dberror { |
347 |
$main::lxdebug->enter_sub(); |
|
348 |
|
|
349 | 347 |
my ($self, $msg) = @_; |
350 | 348 |
|
351 |
$self->error("$msg\n" . $DBI::errstr); |
|
352 |
|
|
353 |
$main::lxdebug->leave_sub(); |
|
349 |
die SL::X::DBError->new( |
|
350 |
msg => $msg, |
|
351 |
error => $DBI::errstr, |
|
352 |
); |
|
354 | 353 |
} |
355 | 354 |
|
356 | 355 |
sub isblank { |
SL/X.pm | ||
---|---|---|
5 | 5 |
use Exception::Lite qw(declareExceptionClass); |
6 | 6 |
|
7 | 7 |
declareExceptionClass('SL::X::FormError'); |
8 |
declareExceptionClass('SL::X::DBHookError', [ '%s hook \'%s\' for object type \'%s\' failed', qw(when hook object_type object) ]); |
|
8 |
declareExceptionClass('SL::X::DBError'); |
|
9 |
declareExceptionClass('SL::X::DBHookError', 'SL::X::DBError', [ '%s hook \'%s\' for object type \'%s\' failed', qw(when hook object_type object) ]); |
|
10 |
declareExceptionClass('SL::X::DBRoseError', 'SL::X::DBError', [ '\'%s\' in object of type \'%s\' occured', qw(error class) ]); |
|
11 |
declareExceptionClass('SL::X::DBUtilsError', 'SL::X::DBError', [ '%s: %s', qw(msg error) ]); |
|
9 | 12 |
|
10 | 13 |
1; |
t/db_helper/with_transaction.t | ||
---|---|---|
1 |
use Test::More tests => 17; |
|
2 |
use Test::Exception; |
|
3 |
|
|
4 |
use strict; |
|
5 |
|
|
6 |
use lib 't'; |
|
7 |
use utf8; |
|
8 |
|
|
9 |
use Carp; |
|
10 |
use Data::Dumper; |
|
11 |
use Support::TestSetup; |
|
12 |
use SL::DB::Part; |
|
13 |
use SL::Dev::Part; |
|
14 |
|
|
15 |
Support::TestSetup::login(); |
|
16 |
|
|
17 |
SL::DB::Manager::Part->delete_all(all => 1, cascade => 1); |
|
18 |
|
|
19 |
# silence the Test::Harness warn handler |
|
20 |
local $SIG{__WARN__} = sub {}; |
|
21 |
|
|
22 |
# test simple transaction |
|
23 |
|
|
24 |
my $part = create_part(); |
|
25 |
SL::DB->client->with_transaction(sub { |
|
26 |
$part->save; |
|
27 |
ok 1, 'part saved'; |
|
28 |
1; |
|
29 |
}) or do { |
|
30 |
ok 0, 'error saving part'; |
|
31 |
}; |
|
32 |
|
|
33 |
# test failing transaction |
|
34 |
my $part2 = create_part(partnumber => $part->partnumber); # woops, duplicate partnumber |
|
35 |
SL::DB->client->with_transaction(sub { |
|
36 |
$part2->save; |
|
37 |
ok 0, 'part saved'; |
|
38 |
1; |
|
39 |
}) or do { |
|
40 |
ok 1, 'saving part with duplicate partnumber generates graceful error'; |
|
41 |
}; |
|
42 |
|
|
43 |
# test transaction with run time exception |
|
44 |
dies_ok { |
|
45 |
SL::DB->client->with_transaction(sub { |
|
46 |
$part->method_that_does_not_exist; |
|
47 |
ok 0, 'this should have died'; |
|
48 |
1; |
|
49 |
}) or do { |
|
50 |
ok 0, 'this should not get here'; |
|
51 |
}; |
|
52 |
} 'method not found in transaction died as expect'; |
|
53 |
|
|
54 |
# test transaction with hook error |
|
55 |
# TODO - not possible to test without locally adding hooks in run time |
|
56 |
|
|
57 |
# test if error gets correctly stored in db->error |
|
58 |
$part2 = create_part(partnumber => $part->partnumber); # woops, duplicate partnumber |
|
59 |
SL::DB->client->with_transaction(sub { |
|
60 |
$part2->save; |
|
61 |
ok 0, 'part saved'; |
|
62 |
1; |
|
63 |
}) or do { |
|
64 |
like(SL::DB->client->error, qr/duplicate key value violates unique constraint/, 'error is in db->error'); |
|
65 |
}; |
|
66 |
|
|
67 |
# test stacked transactions |
|
68 |
# 1. test that it works |
|
69 |
SL::DB->client->with_transaction(sub { |
|
70 |
$part->sellprice(1); |
|
71 |
$part->save; |
|
72 |
|
|
73 |
SL::DB->client->with_transaction(sub { |
|
74 |
$part->sellprice(2); |
|
75 |
$part->save; |
|
76 |
}) or do { |
|
77 |
ok 0, 'error saving part'; |
|
78 |
}; |
|
79 |
|
|
80 |
$part->sellprice(3); |
|
81 |
$part->save; |
|
82 |
1; |
|
83 |
}) or do { |
|
84 |
ok 0, 'error saving part'; |
|
85 |
}; |
|
86 |
|
|
87 |
$part->load; |
|
88 |
is $part->sellprice, "3.00000", 'part saved'; |
|
89 |
|
|
90 |
# 2. with a transaction rollback |
|
91 |
SL::DB->client->with_transaction(sub { |
|
92 |
$part->sellprice(1); |
|
93 |
$part2->save; |
|
94 |
$part->save; |
|
95 |
|
|
96 |
SL::DB->client->with_transaction(sub { |
|
97 |
$part->sellprice(2); |
|
98 |
$part->save; |
|
99 |
}) or do { |
|
100 |
ok 0, 'should not get here'; |
|
101 |
}; |
|
102 |
|
|
103 |
$part->sellprice(3); |
|
104 |
$part->save; |
|
105 |
ok 0, 'should not get here'; |
|
106 |
1; |
|
107 |
}) or do { |
|
108 |
ok 1, 'sql error skips rest of the transaction'; |
|
109 |
}; |
|
110 |
|
|
111 |
|
|
112 |
SL::DB->client->with_transaction(sub { |
|
113 |
$part->sellprice(1); |
|
114 |
$part->save; |
|
115 |
|
|
116 |
SL::DB->client->with_transaction(sub { |
|
117 |
$part->sellprice(2); |
|
118 |
$part->save; |
|
119 |
$part2->save; |
|
120 |
}) or do { |
|
121 |
ok 0, 'should not get here'; |
|
122 |
}; |
|
123 |
|
|
124 |
$part->sellprice(3); |
|
125 |
$part->save; |
|
126 |
ok 0, 'should not get here'; |
|
127 |
1; |
|
128 |
}) or do { |
|
129 |
ok 1, 'sql error in nested transaction rolls back'; |
|
130 |
like(SL::DB->client->error, qr/duplicate key value violates unique constraint/, 'error from nested transaction is in db->error'); |
|
131 |
}; |
|
132 |
|
|
133 |
$part->load; |
|
134 |
is $part->sellprice, "3.00000", 'saved part is not affected'; |
|
135 |
|
|
136 |
|
|
137 |
|
|
138 |
SL::DB->client->with_transaction(sub { |
|
139 |
$part->sellprice(1); |
|
140 |
$part->save; |
|
141 |
|
|
142 |
SL::DB->client->with_transaction(sub { |
|
143 |
$part->sellprice(2); |
|
144 |
$part->save; |
|
145 |
}) or do { |
|
146 |
ok 0, 'should not get here'; |
|
147 |
}; |
|
148 |
|
|
149 |
$part->sellprice(4); |
|
150 |
$part->save; |
|
151 |
$part2->save; |
|
152 |
ok 0, 'should not get here'; |
|
153 |
1; |
|
154 |
}) or do { |
|
155 |
ok 1, 'sql error after nested transaction rolls back'; |
|
156 |
}; |
|
157 |
|
|
158 |
$part->load; |
|
159 |
is $part->sellprice, "3.00000", 'saved part is not affected'; |
|
160 |
|
|
161 |
eval { |
|
162 |
SL::DB->client->with_transaction(sub { |
|
163 |
$part->sellprice(1); |
|
164 |
$part->not_existing_function(); |
|
165 |
$part->save; |
|
166 |
|
|
167 |
SL::DB->client->with_transaction(sub { |
|
168 |
$part->sellprice(2); |
|
169 |
$part->save; |
|
170 |
}) or do { |
|
171 |
ok 0, 'should not get here'; |
|
172 |
}; |
|
173 |
|
|
174 |
$part->sellprice(4); |
|
175 |
$part->save; |
|
176 |
ok 0, 'should not get here'; |
|
177 |
1; |
|
178 |
}) or do { |
|
179 |
ok 0, 'should not get here'; |
|
180 |
}; |
|
181 |
1; |
|
182 |
} or do { |
|
183 |
ok 1, 'runtime exception error before nested transaction rolls back'; |
|
184 |
}; |
|
185 |
|
|
186 |
$part->load; |
|
187 |
is $part->sellprice, "3.00000", 'saved part is not affected'; |
|
188 |
|
|
189 |
eval { |
|
190 |
SL::DB->client->with_transaction(sub { |
|
191 |
$part->sellprice(1); |
|
192 |
$part->save; |
|
193 |
|
|
194 |
SL::DB->client->with_transaction(sub { |
|
195 |
$part->sellprice(2); |
|
196 |
$part->not_existing_function(); |
|
197 |
$part->save; |
|
198 |
}) or do { |
|
199 |
ok 0, 'should not get here'; |
|
200 |
}; |
|
201 |
|
|
202 |
$part->sellprice(4); |
|
203 |
$part->save; |
|
204 |
ok 0, 'should not get here'; |
|
205 |
1; |
|
206 |
}) or do { |
|
207 |
ok 0, 'should not get here'; |
|
208 |
}; |
|
209 |
1; |
|
210 |
} or do { |
|
211 |
ok 1, 'runtime exception error in nested transaction rolls back'; |
|
212 |
}; |
|
213 |
|
|
214 |
$part->load; |
|
215 |
is $part->sellprice, "3.00000", 'saved part is not affected'; |
|
216 |
|
|
217 |
|
|
218 |
eval { |
|
219 |
SL::DB->client->with_transaction(sub { |
|
220 |
$part->sellprice(1); |
|
221 |
$part->save; |
|
222 |
|
|
223 |
SL::DB->client->with_transaction(sub { |
|
224 |
$part->sellprice(2); |
|
225 |
$part->save; |
|
226 |
}) or do { |
|
227 |
ok 0, 'should not get here'; |
|
228 |
}; |
|
229 |
|
|
230 |
$part->sellprice(4); |
|
231 |
$part->save; |
|
232 |
$part->not_existing_function(); |
|
233 |
ok 0, 'should not get here'; |
|
234 |
1; |
|
235 |
}) or do { |
|
236 |
ok 0, 'should not get here'; |
|
237 |
}; |
|
238 |
1; |
|
239 |
} or do { |
|
240 |
ok 1, 'runtime exception error after nested transaction rolls back'; |
|
241 |
}; |
|
242 |
|
|
243 |
$part->load; |
|
244 |
is $part->sellprice, "3.00000", 'saved part is not affected'; |
|
245 |
|
Auch abrufbar als: Unified diff
DB Transaktionen - Fehler nach oben durchreichen