Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 360 lines (339 sloc) 14.984 kB
db045db @dhh Initial
dhh authored
1 require 'thread'
2
3 module ActiveRecord
a293278 @lifo Merge docrails
lifo authored
4 # See ActiveRecord::Transactions::ClassMethods for documentation.
5 module Transactions
4e50a35 @josh Break up DependencyModule's dual function of providing a "depend_on" …
josh authored
6 extend ActiveSupport::Concern
a2875be @brynary Use DependencyModule for included hooks in ActiveRecord
brynary authored
7
a677da2 @dhh Added rollbacks of transactions if they're active as the dispatcher i…
dhh authored
8 class TransactionError < ActiveRecordError # :nodoc:
9 end
10
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
11 included do
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
12 define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
13 end
6445441 @rizwanreza Adds title to the rest of the files in activerecord/lib
rizwanreza authored
14 # = Active Record Transactions
15 #
a293278 @lifo Merge docrails
lifo authored
16 # Transactions are protective blocks where SQL statements are only permanent
17 # if they can all succeed as one atomic action. The classic example is a
18 # transfer between two accounts where you can only have a deposit if the
19 # withdrawal succeeded and vice versa. Transactions enforce the integrity of
20 # the database and guard the data against program errors or database
21 # break-downs. So basically you should use transaction blocks whenever you
22 # have a number of statements that must be executed together or not at all.
6445441 @rizwanreza Adds title to the rest of the files in activerecord/lib
rizwanreza authored
23 #
24 # For example:
db045db @dhh Initial
dhh authored
25 #
a293278 @lifo Merge docrails
lifo authored
26 # ActiveRecord::Base.transaction do
db045db @dhh Initial
dhh authored
27 # david.withdrawal(100)
28 # mary.deposit(100)
29 # end
30 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
31 # This example will only take money from David and give it to Mary if neither
32 # +withdrawal+ nor +deposit+ raise an exception. Exceptions will force a
33 # ROLLBACK that returns the database to the state before the transaction
34 # began. Be aware, though, that the objects will _not_ have their instance
a293278 @lifo Merge docrails
lifo authored
35 # data returned to their pre-transactional state.
db045db @dhh Initial
dhh authored
36 #
98dc582 @lifo Merge docrails.
lifo authored
37 # == Different Active Record classes in a single transaction
4f59aac Explain semantics of having several different AR instances in a trans…
Marcel Molina authored
38 #
98dc582 @lifo Merge docrails.
lifo authored
39 # Though the transaction class method is called on some Active Record class,
4f59aac Explain semantics of having several different AR instances in a trans…
Marcel Molina authored
40 # the objects within the transaction block need not all be instances of
a293278 @lifo Merge docrails
lifo authored
41 # that class. This is because transactions are per-database connection, not
42 # per-model.
43 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
44 # In this example a +balance+ record is transactionally saved even
45 # though +transaction+ is called on the +Account+ class:
4f59aac Explain semantics of having several different AR instances in a trans…
Marcel Molina authored
46 #
47 # Account.transaction do
96fa4a2 Make transaction documentation example more realistic
Marcel Molina authored
48 # balance.save!
49 # account.save!
4f59aac Explain semantics of having several different AR instances in a trans…
Marcel Molina authored
50 # end
51 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
52 # The +transaction+ method is also available as a model instance method.
53 # For example, you can also do this:
a293278 @lifo Merge docrails
lifo authored
54 #
55 # balance.transaction do
56 # balance.save!
57 # account.save!
58 # end
59 #
db045db @dhh Initial
dhh authored
60 # == Transactions are not distributed across database connections
61 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
62 # A transaction acts on a single database connection. If you have
db045db @dhh Initial
dhh authored
63 # multiple class-specific databases, the transaction will not protect
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
64 # interaction among them. One workaround is to begin a transaction
db045db @dhh Initial
dhh authored
65 # on each class whose models you alter:
66 #
67 # Student.transaction do
68 # Course.transaction do
69 # course.enroll(student)
70 # student.units += course.units
71 # end
72 # end
73 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
74 # This is a poor solution, but fully distributed transactions are beyond
db045db @dhh Initial
dhh authored
75 # the scope of Active Record.
76 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
77 # == +save+ and +destroy+ are automatically wrapped in a transaction
db045db @dhh Initial
dhh authored
78 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
79 # Both +save+ and +destroy+ come wrapped in a transaction that ensures
80 # that whatever you do in validations or callbacks will happen under its
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
81 # protected cover. So you can use validations to check for values that
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
82 # the transaction depends on or you can raise exceptions in the callbacks
83 # to rollback, including <tt>after_*</tt> callbacks.
84 #
85 # As a consequence changes to the database are not seen outside your connection
86 # until the operation is complete. For example, if you try to update the index
87 # of a search engine in +after_save+ the indexer won't see the updated record.
88 # The +after_commit+ callback is the only one that is triggered once the update
89 # is committed. See below.
db045db @dhh Initial
dhh authored
90 #
6e75455 @lifo Merge docrails changes
lifo authored
91 # == Exception handling and rolling back
db045db @dhh Initial
dhh authored
92 #
a293278 @lifo Merge docrails
lifo authored
93 # Also have in mind that exceptions thrown within a transaction block will
94 # be propagated (after triggering the ROLLBACK), so you should be ready to
95 # catch those in your application code.
6e75455 @lifo Merge docrails changes
lifo authored
96 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
97 # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger
a293278 @lifo Merge docrails
lifo authored
98 # a ROLLBACK when raised, but not be re-raised by the transaction block.
99 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
100 # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions
101 # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an
a293278 @lifo Merge docrails
lifo authored
102 # error occurred at the database level, for example when a unique constraint
103 # is violated. On some database systems, such as PostgreSQL, database errors
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
104 # inside a transaction cause the entire transaction to become unusable
a293278 @lifo Merge docrails
lifo authored
105 # until it's restarted from the beginning. Here is an example which
106 # demonstrates the problem:
107 #
108 # # Suppose that we have a Number model with a unique column called 'i'.
109 # Number.transaction do
110 # Number.create(:i => 0)
111 # begin
112 # # This will raise a unique constraint error...
113 # Number.create(:i => 0)
114 # rescue ActiveRecord::StatementInvalid
115 # # ...which we ignore.
116 # end
da840d1 @bdurand Add after_commit and after_rollback callbacks to ActiveRecord that ar…
bdurand authored
117 #
a293278 @lifo Merge docrails
lifo authored
118 # # On PostgreSQL, the transaction is now unusable. The following
119 # # statement will cause a PostgreSQL error, even though the unique
120 # # constraint is no longer violated:
121 # Number.create(:i => 1)
122 # # => "PGError: ERROR: current transaction is aborted, commands
123 # # ignored until end of transaction block"
124 # end
125 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
126 # One should restart the entire transaction if an
127 # <tt>ActiveRecord::StatementInvalid</tt> occurred.
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
128 #
129 # == Nested transactions
130 #
6433c93 @fxn edit pass in the transactions preamble rdoc
fxn authored
131 # +transaction+ calls can be nested. By default, this makes all database
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
132 # statements in the nested transaction block become part of the parent
9254750 @fxn reviews commit 53bbbcc
fxn authored
133 # transaction. For example, the following behavior may be surprising:
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
134 #
135 # User.transaction do
136 # User.create(:username => 'Kotori')
137 # User.transaction do
138 # User.create(:username => 'Nemu')
139 # raise ActiveRecord::Rollback
140 # end
141 # end
da840d1 @bdurand Add after_commit and after_rollback callbacks to ActiveRecord that ar…
bdurand authored
142 #
9254750 @fxn reviews commit 53bbbcc
fxn authored
143 # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
144 # exception in the nested block does not issue a ROLLBACK. Since these exceptions
145 # are captured in transaction blocks, the parent block does not see it and the
146 # real transaction is committed.
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
147 #
9254750 @fxn reviews commit 53bbbcc
fxn authored
148 # In order to get a ROLLBACK for the nested transaction you may ask for a real
149 # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong,
150 # the database rolls back to the beginning of the sub-transaction without rolling
151 # back the parent transaction. If we add it to the previous example:
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
152 #
153 # User.transaction do
154 # User.create(:username => 'Kotori')
ab0ce05 @jeremy Introduce transaction_joinable flag to mark that the fixtures transac…
jeremy authored
155 # User.transaction(:requires_new => true) do
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
156 # User.create(:username => 'Nemu')
157 # raise ActiveRecord::Rollback
158 # end
159 # end
da840d1 @bdurand Add after_commit and after_rollback callbacks to ActiveRecord that ar…
bdurand authored
160 #
9254750 @fxn reviews commit 53bbbcc
fxn authored
161 # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.)
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
162 #
163 # Most databases don't support true nested transactions. At the time of
164 # writing, the only database that we're aware of that supports true nested
165 # transactions, is MS-SQL. Because of this, Active Record emulates nested
9254750 @fxn reviews commit 53bbbcc
fxn authored
166 # transactions by using savepoints on MySQL and PostgreSQL. See
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
167 # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
168 # for more information about savepoints.
169 #
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
170 # === Callbacks
171 #
172 # There are two types of callbacks associated with committing and rolling back transactions:
173 # +after_commit+ and +after_rollback+.
174 #
175 # +after_commit+ callbacks are called on every record saved or destroyed within a
176 # transaction immediately after the transaction is committed. +after_rollback+ callbacks
177 # are called on every record saved or destroyed within a transaction immediately after the
178 # transaction or savepoint is rolled back.
179 #
180 # These callbacks are useful for interacting with other systems since you will be guaranteed
181 # that the callback is only executed when the database is in a permanent state. For example,
182 # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
183 # within a transaction could trigger the cache to be regenerated before the database is updated.
184 #
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
185 # === Caveats
186 #
187 # If you're on MySQL, then do not use DDL operations in nested transactions
188 # blocks that are emulated with savepoints. That is, do not execute statements
189 # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
1ff954a @fxn after_(commit|rollback) rdoc, edit pass
fxn authored
190 # releases all savepoints upon executing a DDL operation. When +transaction+
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
191 # is finished and tries to release the savepoint it created earlier, a
192 # database error will occur because the savepoint has already been
193 # automatically released. The following example demonstrates the problem:
da840d1 @bdurand Add after_commit and after_rollback callbacks to ActiveRecord that ar…
bdurand authored
194 #
ab0ce05 @jeremy Introduce transaction_joinable flag to mark that the fixtures transac…
jeremy authored
195 # Model.connection.transaction do # BEGIN
196 # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
197 # Model.connection.create_table(...) # active_record_1 now automatically released
198 # end # RELEASE savepoint active_record_1
199 # # ^^^^ BOOM! database error!
e916aa7 @FooBarWidget Rename ActiveRecord::Base#transaction's :force option to :nest. Impro…
FooBarWidget authored
200 # end
632bbbf @lifo Merge docrails
lifo authored
201 #
202 # Note that "TRUNCATE" is also a MySQL DDL statement!
50562c2 @dhh Restored thread safety to Active Record [andreas]
dhh authored
203 module ClassMethods
6e75455 @lifo Merge docrails changes
lifo authored
204 # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
b3420f5 Implement savepoints.
Jonathan Viney authored
205 def transaction(options = {}, &block)
ab0ce05 @jeremy Introduce transaction_joinable flag to mark that the fixtures transac…
jeremy authored
206 # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
207 connection.transaction(options, &block)
db045db @dhh Initial
dhh authored
208 end
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
209
210 def after_commit(*args, &block)
211 options = args.last
212 if options.is_a?(Hash) && options[:on]
213 options[:if] = Array.wrap(options[:if])
214 options[:if] << "transaction_include_action?(:#{options[:on]})"
215 end
216 set_callback(:commit, :after, *args, &block)
217 end
218
219 def after_rollback(*args, &block)
220 options = args.last
221 if options.is_a?(Hash) && options[:on]
222 options[:if] = Array.wrap(options[:if])
223 options[:if] << "transaction_include_action?(:#{options[:on]})"
224 end
225 set_callback(:rollback, :after, *args, &block)
226 end
db045db @dhh Initial
dhh authored
227 end
228
a293278 @lifo Merge docrails
lifo authored
229 # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
f87db85 @NZKoz Remove deprecated object transactions. People relying on this functi…
NZKoz authored
230 def transaction(&block)
231 self.class.transaction(&block)
db045db @dhh Initial
dhh authored
232 end
233
d916c62 @wycats eliminate alias_method_chain from ActiveRecord
wycats authored
234 def destroy #:nodoc:
235 with_transaction_returning_status { super }
db045db @dhh Initial
dhh authored
236 end
629b8af @jeremy Wrap save! in a transaction. Closes #6324.
jeremy authored
237
d916c62 @wycats eliminate alias_method_chain from ActiveRecord
wycats authored
238 def save(*) #:nodoc:
239 rollback_active_record_state! do
240 with_transaction_returning_status { super }
241 end
db045db @dhh Initial
dhh authored
242 end
629b8af @jeremy Wrap save! in a transaction. Closes #6324.
jeremy authored
243
d916c62 @wycats eliminate alias_method_chain from ActiveRecord
wycats authored
244 def save!(*) #:nodoc:
245 with_transaction_returning_status { super }
1af2022 @technoweenie Rollback #new_record? and #id values for created records that rollbac…
technoweenie authored
246 end
3b6555a @jeremy Fix new_record? and id rollback. Closes #6910.
jeremy authored
247
75015d1 @spastorino Revert f1c13b0dd7b22b5f6289ca1a09f1d7a8c7c8584b
spastorino authored
248 # Reset id and @new_record if the transaction rolls back.
3b6555a @jeremy Fix new_record? and id rollback. Closes #6910.
jeremy authored
249 def rollback_active_record_state!
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
250 remember_transaction_record_state
3b6555a @jeremy Fix new_record? and id rollback. Closes #6910.
jeremy authored
251 yield
252 rescue Exception
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
253 restore_transaction_record_state
254 raise
255 ensure
256 clear_transaction_record_state
257 end
258
259 # Call the after_commit callbacks
260 def committed! #:nodoc:
261 _run_commit_callbacks
262 ensure
263 clear_transaction_record_state
264 end
265
266 # Call the after rollback callbacks. The restore_state argument indicates if the record
267 # state should be rolled back to the beginning or just to the last savepoint.
268 def rolledback!(force_restore_state = false) #:nodoc:
269 _run_rollback_callbacks
270 ensure
271 restore_transaction_record_state(force_restore_state)
272 end
273
274 # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
275 # can be called.
276 def add_to_transaction
277 if self.class.connection.add_transaction_record(self)
278 remember_transaction_record_state
8b5f4e4 @jeremy Ruby 1.9 compat: fix warnings, shadowed block vars, and unitialized i…
jeremy authored
279 end
629b8af @jeremy Wrap save! in a transaction. Closes #6324.
jeremy authored
280 end
e02f0dc @fxn Rollback the transaction when a before_* callback returns false.
fxn authored
281
282 # Executes +method+ within a transaction and captures its return value as a
283 # status flag. If the status is true the transaction is committed, otherwise
284 # a ROLLBACK is issued. In any case the status flag is returned.
a293278 @lifo Merge docrails
lifo authored
285 #
286 # This method is available within the context of an ActiveRecord::Base
287 # instance.
d916c62 @wycats eliminate alias_method_chain from ActiveRecord
wycats authored
288 def with_transaction_returning_status
e02f0dc @fxn Rollback the transaction when a before_* callback returns false.
fxn authored
289 status = nil
455c7f9 @fcheung Don't use the transaction instance method so that people with has_one…
fcheung authored
290 self.class.transaction do
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
291 add_to_transaction
d916c62 @wycats eliminate alias_method_chain from ActiveRecord
wycats authored
292 status = yield
e02f0dc @fxn Rollback the transaction when a before_* callback returns false.
fxn authored
293 raise ActiveRecord::Rollback unless status
294 end
295 status
296 end
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
297
298 protected
299
300 # Save the new record state and id of a record so it can be restored later if a transaction fails.
301 def remember_transaction_record_state #:nodoc
302 @_start_transaction_state ||= {}
75015d1 @spastorino Revert f1c13b0dd7b22b5f6289ca1a09f1d7a8c7c8584b
spastorino authored
303 unless @_start_transaction_state.include?(:new_record)
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
304 @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
75015d1 @spastorino Revert f1c13b0dd7b22b5f6289ca1a09f1d7a8c7c8584b
spastorino authored
305 @_start_transaction_state[:new_record] = @new_record
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
306 end
307 unless @_start_transaction_state.include?(:destroyed)
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
308 @_start_transaction_state[:destroyed] = @destroyed
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
309 end
310 @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
311 end
312
313 # Clear the new record state and id of a record.
314 def clear_transaction_record_state #:nodoc
315 if defined?(@_start_transaction_state)
316 @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
317 remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
318 end
319 end
320
321 # Restore the new record state and id of a record that was previously saved by a call to save_record_state.
322 def restore_transaction_record_state(force = false) #:nodoc
323 if defined?(@_start_transaction_state)
324 @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
325 if @_start_transaction_state[:level] < 1
326 restore_state = remove_instance_variable(:@_start_transaction_state)
327 if restore_state
237165f @bdurand Fix bug with rolling back frozen attributes.
bdurand authored
328 @attributes = @attributes.dup if @attributes.frozen?
75015d1 @spastorino Revert f1c13b0dd7b22b5f6289ca1a09f1d7a8c7c8584b
spastorino authored
329 @new_record = restore_state[:new_record]
b070739 @jeremy Revert "Temporarily revert "Update after_commit and after_rollback do…
jeremy authored
330 @destroyed = restore_state[:destroyed]
331 if restore_state[:id]
332 self.id = restore_state[:id]
333 else
334 @attributes.delete(self.class.primary_key)
335 @attributes_cache.delete(self.class.primary_key)
336 end
337 end
338 end
339 end
340 end
341
342 # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
343 def transaction_record_state(state) #:nodoc
344 @_start_transaction_state[state] if defined?(@_start_transaction_state)
345 end
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
346
347 # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
348 def transaction_include_action?(action) #:nodoc
349 case action
350 when :create
75015d1 @spastorino Revert f1c13b0dd7b22b5f6289ca1a09f1d7a8c7c8584b
spastorino authored
351 transaction_record_state(:new_record)
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
352 when :destroy
353 destroyed?
354 when :update
75015d1 @spastorino Revert f1c13b0dd7b22b5f6289ca1a09f1d7a8c7c8584b
spastorino authored
355 !(transaction_record_state(:new_record) || destroyed?)
2500e6a @bdurand Make logic for after_commit and after_rollback :on option work like i…
bdurand authored
356 end
357 end
db045db @dhh Initial
dhh authored
358 end
098fa94 @dhh Fixed documentation snafus #575, #576, #577, #585
dhh authored
359 end
Something went wrong with that request. Please try again.