Merge branch 'patches'
This commit is contained in:
commit
33ed19f428
@ -1,5 +1,7 @@
|
||||
*Edge*
|
||||
|
||||
* before_save, before_validation and before_destroy callbacks that return false will now ROLLBACK the transaction. Previously this would have been committed before the processing was aborted. #891 [Xavier Noria]
|
||||
|
||||
* Transactional migrations for databases which support them. #834 [divoxx, Adam Wiggins, Tarmo Tänav]
|
||||
|
||||
* Set config.active_record.timestamped_migrations = false to have migrations with numeric prefix instead of UTC timestamp. #446. [Andrew Stone, Nik Wakelin]
|
||||
|
@ -169,6 +169,18 @@ module ActiveRecord
|
||||
# If a <tt>before_*</tt> callback returns +false+, all the later callbacks and the associated action are cancelled. If an <tt>after_*</tt> callback returns
|
||||
# +false+, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks
|
||||
# defined as methods on the model, which are called last.
|
||||
#
|
||||
# == Transactions
|
||||
#
|
||||
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
|
||||
# within a transaction. That includes <tt>after_*</tt> hooks. If everything
|
||||
# goes fine a COMMIT is executed once the chain has been completed.
|
||||
#
|
||||
# If a <tt>before_*</tt> callback cancels the action a ROLLBACK is issued. You
|
||||
# can also trigger a ROLLBACK raising an exception in any of the callbacks,
|
||||
# including <tt>after_*</tt> hooks. Note, however, that in that case the client
|
||||
# needs to be aware of it because an ordinary +save+ will raise such exception
|
||||
# instead of quietly returning +false+.
|
||||
module Callbacks
|
||||
CALLBACKS = %w(
|
||||
after_find after_initialize before_save after_save before_create after_create before_update after_update before_validation
|
||||
|
@ -91,11 +91,11 @@ def transaction(&block)
|
||||
end
|
||||
|
||||
def destroy_with_transactions #:nodoc:
|
||||
transaction { destroy_without_transactions }
|
||||
with_transaction_returning_status(:destroy_without_transactions)
|
||||
end
|
||||
|
||||
def save_with_transactions(perform_validation = true) #:nodoc:
|
||||
rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
|
||||
rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
|
||||
end
|
||||
|
||||
def save_with_transactions! #:nodoc:
|
||||
@ -118,5 +118,17 @@ def rollback_active_record_state!
|
||||
end
|
||||
raise
|
||||
end
|
||||
|
||||
# Executes +method+ within a transaction and captures its return value as a
|
||||
# status flag. If the status is true the transaction is committed, otherwise
|
||||
# a ROLLBACK is issued. In any case the status flag is returned.
|
||||
def with_transaction_returning_status(method, *args)
|
||||
status = nil
|
||||
transaction do
|
||||
status = send(method, *args)
|
||||
raise ActiveRecord::Rollback unless status
|
||||
end
|
||||
status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,6 +2,7 @@
|
||||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
require 'models/developer'
|
||||
require 'models/book'
|
||||
|
||||
class TransactionTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
@ -86,8 +87,7 @@ def test_failing_on_exception
|
||||
assert Topic.find(2).approved?, "Second should still be approved"
|
||||
end
|
||||
|
||||
|
||||
def test_callback_rollback_in_save
|
||||
def test_raising_exception_in_callback_rollbacks_in_save
|
||||
add_exception_raising_after_save_callback_to_topic
|
||||
|
||||
begin
|
||||
@ -102,6 +102,54 @@ def test_callback_rollback_in_save
|
||||
end
|
||||
end
|
||||
|
||||
def test_cancellation_from_before_destroy_rollbacks_in_destroy
|
||||
add_cancelling_before_destroy_with_db_side_effect_to_topic
|
||||
begin
|
||||
nbooks_before_destroy = Book.count
|
||||
status = @first.destroy
|
||||
assert !status
|
||||
assert_nothing_raised(ActiveRecord::RecordNotFound) { @first.reload }
|
||||
assert_equal nbooks_before_destroy, Book.count
|
||||
ensure
|
||||
remove_cancelling_before_destroy_with_db_side_effect_to_topic
|
||||
end
|
||||
end
|
||||
|
||||
def test_cancellation_from_before_filters_rollbacks_in_save
|
||||
%w(validation save).each do |filter|
|
||||
send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
begin
|
||||
nbooks_before_save = Book.count
|
||||
original_author_name = @first.author_name
|
||||
@first.author_name += '_this_should_not_end_up_in_the_db'
|
||||
status = @first.save
|
||||
assert !status
|
||||
assert_equal original_author_name, @first.reload.author_name
|
||||
assert_equal nbooks_before_save, Book.count
|
||||
ensure
|
||||
send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_cancellation_from_before_filters_rollbacks_in_save!
|
||||
%w(validation save).each do |filter|
|
||||
send("add_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
begin
|
||||
nbooks_before_save = Book.count
|
||||
original_author_name = @first.author_name
|
||||
@first.author_name += '_this_should_not_end_up_in_the_db'
|
||||
@first.save!
|
||||
flunk
|
||||
rescue => e
|
||||
assert_equal original_author_name, @first.reload.author_name
|
||||
assert_equal nbooks_before_save, Book.count
|
||||
ensure
|
||||
send("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_callback_rollback_in_create
|
||||
new_topic = Topic.new(
|
||||
:title => "A new topic",
|
||||
@ -221,6 +269,16 @@ def add_exception_raising_after_create_callback_to_topic
|
||||
def remove_exception_raising_after_create_callback_to_topic
|
||||
Topic.class_eval { remove_method :after_create }
|
||||
end
|
||||
|
||||
%w(validation save destroy).each do |filter|
|
||||
define_method("add_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
|
||||
Topic.class_eval "def before_#{filter}() Book.create; false end"
|
||||
end
|
||||
|
||||
define_method("remove_cancelling_before_#{filter}_with_db_side_effect_to_topic") do
|
||||
Topic.class_eval "remove_method :before_#{filter}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if current_adapter?(:PostgreSQLAdapter)
|
||||
|
Loading…
Reference in New Issue
Block a user