From da840d13da865331297d5287391231b1ed39721b Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Tue, 2 Jun 2009 14:42:22 -0500 Subject: Add after_commit and after_rollback callbacks to ActiveRecord that are called after transactions either commit or rollback on all records saved or destroyed in the transaction. [#2991 state:committed] Signed-off-by: Jeremy Kemper --- .../test/cases/transaction_callbacks_test.rb | 244 +++++++++++++++++++++ activerecord/test/cases/transactions_test.rb | 61 ++++-- 2 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 activerecord/test/cases/transaction_callbacks_test.rb (limited to 'activerecord/test') diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb new file mode 100644 index 0000000000..c2c5fd3b05 --- /dev/null +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -0,0 +1,244 @@ +require "cases/helper" +require 'models/topic' +require 'models/reply' + +class TransactionCallbacksTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + fixtures :topics + + class TopicWithCallbacks < ActiveRecord::Base + set_table_name :topics + + after_commit{|record| record.send(:do_after_commit, nil)} + after_commit_on_create{|record| record.send(:do_after_commit, :create)} + after_commit_on_update{|record| record.send(:do_after_commit, :update)} + after_commit_on_destroy{|record| record.send(:do_after_commit, :destroy)} + after_rollback{|record| record.send(:do_after_rollback, nil)} + after_rollback_on_create{|record| record.send(:do_after_rollback, :create)} + after_rollback_on_update{|record| record.send(:do_after_rollback, :update)} + after_rollback_on_destroy{|record| record.send(:do_after_rollback, :destroy)} + + def history + @history ||= [] + end + + def after_commit_block(on = nil, &block) + @after_commit ||= {} + @after_commit[on] ||= [] + @after_commit[on] << block + end + + def after_rollback_block(on = nil, &block) + @after_rollback ||= {} + @after_rollback[on] ||= [] + @after_rollback[on] << block + end + + def do_after_commit(on) + blocks = @after_commit[on] if defined?(@after_commit) + blocks.each{|b| b.call(self)} if blocks + end + + def do_after_rollback(on) + blocks = @after_rollback[on] if defined?(@after_rollback) + blocks.each{|b| b.call(self)} if blocks + end + end + + def setup + @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id } + end + + def test_call_after_commit_after_transaction_commits + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + @first.save! + assert @first.history, [:after_commit] + end + + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @first.save! + assert @first.history, [:commit_on_update] + end + + def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @first.destroy + assert @first.history, [:commit_on_destroy] + end + + def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create} + @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + @new_record.save! + assert @new_record.history, [:commit_on_create] + end + + def test_call_after_rollback_after_transaction_rollsback + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert @first.history, [:after_rollback] + end + + def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert @first.history, [:rollback_on_update] + end + + def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record + commit_callback = [] + @first.after_commit_block(:create){|r| r.history << :commit_on_create} + @first.after_commit_block(:update){|r| r.history << :commit_on_update} + @first.after_commit_block(:destroy){|r| r.history << :commit_on_update} + @first.after_commit_block(:create){|r| r.history << :rollback_on_create} + @first.after_commit_block(:update){|r| r.history << :rollback_on_update} + @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @first.destroy + raise ActiveRecord::Rollback + end + + assert @first.history, [:rollback_on_destroy] + end + + def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record + @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} + @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} + @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create} + @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update} + @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy} + + Topic.transaction do + @new_record.save! + raise ActiveRecord::Rollback + end + + assert @new_record.history, [:rollback_on_create] + end + + def test_call_after_rollback_when_commit_fails + @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction) + begin + @first.connection.class.class_eval do + def commit_db_transaction; raise "boom!"; end + end + + @first.after_commit_block{|r| r.history << :after_commit} + @first.after_rollback_block{|r| r.history << :after_rollback} + + assert !@first.save rescue nil + assert @first.history == [:after_rollback] + ensure + @first.connection.class.send(:remove_method, :commit_db_transaction) + @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction) + end + end + + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + @first.after_rollback_block{|r| r.rollbacks(1)} + @first.after_commit_block{|r| r.commits(1)} + + def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @second.commits(i=0); @commits ||= 0; @commits += i if i; end + @second.after_rollback_block{|r| r.rollbacks(1)} + @second.after_commit_block{|r| r.commits(1)} + + Topic.transaction do + @first.save! + Topic.transaction(:requires_new => true) do + @second.save! + raise ActiveRecord::Rollback + end + end + + assert 1, @first.commits + assert 0, @first.rollbacks + assert 1, @second.commits + assert 1, @second.rollbacks + end + + def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails + def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + + @second.after_rollback_block{|r| r.rollbacks(1)} + @second.after_commit_block{|r| r.commits(1)} + + Topic.transaction do + @first.save + Topic.transaction(:requires_new => true) do + @first.save! + raise ActiveRecord::Rollback + end + Topic.transaction(:requires_new => true) do + @first.save! + raise ActiveRecord::Rollback + end + end + + assert 1, @first.commits + assert 2, @first.rollbacks + end + + def test_after_transaction_callbacks_should_not_raise_errors + def @first.last_after_transaction_error=(e); @last_transaction_error = e; end + def @first.last_after_transaction_error; @last_transaction_error; end + @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} + @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} + + @first.save! + assert_equal @first.last_after_transaction_error, :commit + + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + + assert_equal @first.last_after_transaction_error, :rollback + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c550030329..958a4e4f94 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -262,22 +262,22 @@ class TransactionTest < ActiveRecord::TestCase assert !@first.reload.approved? assert !@second.reload.approved? end if Topic.connection.supports_savepoints? - + def test_many_savepoints Topic.transaction do @first.content = "One" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Two" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Three" @first.save! - + begin Topic.transaction :requires_new => true do @first.content = "Four" @@ -286,22 +286,22 @@ class TransactionTest < ActiveRecord::TestCase end rescue end - + @three = @first.reload.content raise end rescue end - + @two = @first.reload.content raise end rescue end - + @one = @first.reload.content end - + assert_equal "One", @one assert_equal "Two", @two assert_equal "Three", @three @@ -319,7 +319,34 @@ class TransactionTest < ActiveRecord::TestCase end end end - + + def test_restore_active_record_state_for_all_records_in_a_transaction + topic_1 = Topic.new(:title => 'test_1') + topic_2 = Topic.new(:title => 'test_2') + Topic.transaction do + assert topic_1.save + assert topic_2.save + @first.save + @second.destroy + assert_equal false, topic_1.new_record? + assert_not_nil topic_1.id + assert_equal false, topic_2.new_record? + assert_not_nil topic_2.id + assert_equal false, @first.new_record? + assert_not_nil @first.id + assert_equal true, @second.destroyed? + raise ActiveRecord::Rollback + end + + assert_equal true, topic_1.new_record? + assert_nil topic_1.id + assert_equal true, topic_2.new_record? + assert_nil topic_2.id + assert_equal false, @first.new_record? + assert_not_nil @first.id + assert_equal false, @second.destroyed? + end + if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE) def test_outside_transaction_works assert Topic.connection.outside_transaction? @@ -328,7 +355,7 @@ class TransactionTest < ActiveRecord::TestCase Topic.connection.rollback_db_transaction assert Topic.connection.outside_transaction? end - + def test_rollback_wont_be_executed_if_no_transaction_active assert_raise RuntimeError do Topic.transaction do @@ -338,7 +365,7 @@ class TransactionTest < ActiveRecord::TestCase end end end - + def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active Topic.transaction do Topic.transaction do @@ -358,12 +385,12 @@ class TransactionTest < ActiveRecord::TestCase # # We go back to the connection for the column queries because # Topic.columns is cached and won't report changes to the DB - + assert_nothing_raised do Topic.reset_column_information Topic.connection.add_column('topics', 'stuff', :string) assert Topic.column_names.include?('stuff') - + Topic.reset_column_information Topic.connection.remove_column('topics', 'stuff') assert !Topic.column_names.include?('stuff') @@ -382,6 +409,12 @@ class TransactionTest < ActiveRecord::TestCase end private + def define_callback_method(callback_method) + define_method(callback_method) do + self.history << [callback_method, :method] + end + end + def add_exception_raising_after_save_callback_to_topic Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1 remove_method(:after_save_for_transaction) @@ -440,7 +473,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase def test_automatic_savepoint_in_outer_transaction @first = Topic.find(1) - + begin Topic.transaction do @first.approved = true -- cgit v1.2.3