Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 660c7e53

Von Sven Schöling vor mehr als 7 Jahren hinzugefügt

  • ID 660c7e5312f7fae7766b731f7001e5e8197c6887
  • Vorgänger bce08af4
  • Nachfolger e2332bfd

DB Transaktionen - Fehler nach oben durchreichen

Unterschiede anzeigen:

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