Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #5457: nested_transactions_with_implicit_transaction_config.2.patch

File nested_transactions_with_implicit_transaction_config.2.patch, 15.0 kB (added by tarmo, 1 year ago)

Added Mysql support (in addition to Postgresql)

  • a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

    old new  
    4848      end 
    4949 
    5050      # Wrap a block in a transaction.  Returns result of block. 
    51       def transaction(start_db_transaction = true
     51      def transaction(start_db_transaction = true, savepoint_number = 1
    5252        transaction_open = false 
     53        savepoint_open = false 
    5354        begin 
    5455          if block_given? 
    5556            if start_db_transaction 
    56               begin_db_transaction 
     57              if savepoint_number == 1 
     58                begin_db_transaction 
     59              else 
     60                if create_savepoint(savepoint_number) 
     61                  savepoint_open = true 
     62                end 
     63              end 
    5764              transaction_open = true 
    5865            end 
    5966            yield 
     
    6168        rescue Exception => database_transaction_rollback 
    6269          if transaction_open 
    6370            transaction_open = false 
    64             rollback_db_transaction 
     71            unless savepoint_open 
     72              rollback_db_transaction 
     73            else 
     74              savepoint_open = false 
     75              rollback_to_savepoint(savepoint_number) 
     76            end 
    6577          end 
    6678          raise unless database_transaction_rollback.is_a? ActiveRecord::Rollback 
    6779        end 
    6880      ensure 
    6981        if transaction_open 
    7082          begin 
    71             commit_db_transaction 
     83            unless savepoint_open 
     84              commit_db_transaction 
     85            else 
     86              release_savepoint(savepoint_number) 
     87            end 
    7288          rescue Exception => database_transaction_rollback 
    73             rollback_db_transaction 
     89            unless savepoint_open 
     90              rollback_db_transaction 
     91            else 
     92              rollback_to_savepoint(savepoint_number) 
     93            end 
    7494            raise 
    7595          end 
    7696        end 
     
    86106      # done if the transaction block raises an exception or returns false. 
    87107      def rollback_db_transaction() end 
    88108 
     109      # abstract create_savepoint method that does nothing 
     110      def create_savepoint(sp_number) 
     111      end 
     112 
     113      # abstract rollback_to_savepoint method that does nothing 
     114      def rollback_to_savepoint(sp_number) 
     115      end 
     116 
     117      # abstract release_savepoint method that does nothing 
     118      def release_savepoint(sp_number) 
     119      end 
     120 
    89121      # Alias for #add_limit_offset!. 
    90122      def add_limit!(sql, options) 
    91123        add_limit_offset!(sql, options) if options 
  • a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb

    old new  
    284284        # Transactions aren't supported 
    285285      end 
    286286 
     287      def create_savepoint(sp_number) 
     288        execute("SAVEPOINT rails_nested_transaction_#{sp_number}") 
     289        return true 
     290      rescue Exception 
     291        # savepoints are not supported 
     292      end 
     293 
     294      def rollback_to_savepoint(sp_number) 
     295        execute("ROLLBACK TO SAVEPOINT rails_nested_transaction_#{sp_number}") 
     296      rescue Exception 
     297        # savepoints are not supported 
     298      end 
     299 
     300      def release_savepoint(sp_number) 
     301        execute("RELEASE SAVEPOINT rails_nested_transaction_#{sp_number}") 
     302      rescue Exception 
     303        # savepoints are not supported 
     304      end 
     305 
    287306 
    288307      def add_limit_offset!(sql, options) #:nodoc: 
    289308        if limit = options[:limit] 
  • a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

    old new  
    417417        execute "ROLLBACK" 
    418418      end 
    419419 
     420      def create_savepoint(sp_number) 
     421        execute("SAVEPOINT rails_nested_transaction_#{sp_number}") 
     422        return true 
     423      rescue Exception 
     424        # savepoints are not supported 
     425      end 
     426 
     427      def rollback_to_savepoint(sp_number) 
     428        execute("ROLLBACK TO SAVEPOINT rails_nested_transaction_#{sp_number}") 
     429      rescue Exception 
     430        # savepoints are not supported 
     431      end 
     432 
     433      def release_savepoint(sp_number) 
     434        execute("RELEASE SAVEPOINT rails_nested_transaction_#{sp_number}") 
     435      rescue Exception 
     436        # savepoints are not supported 
     437      end 
     438 
    420439      # SCHEMA STATEMENTS ======================================== 
    421440 
    422441      # Returns the list of all tables in the schema search path or a specified schema. 
  • a/activerecord/lib/active_record/fixtures.rb

    old new  
    557557            load_fixtures 
    558558            @@already_loaded_fixtures[self.class] = @loaded_fixtures 
    559559          end 
     560          Thread.current['transactional_test'] = true 
    560561          ActiveRecord::Base.send :increment_open_transactions 
    561562          ActiveRecord::Base.connection.begin_db_transaction 
    562563 
     
    576577        return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank? 
    577578 
    578579        # Rollback changes if a transaction is active. 
    579         if use_transactional_fixtures? && Thread.current['open_transactions'] != 0 
    580           ActiveRecord::Base.connection.rollback_db_transaction 
    581           Thread.current['open_transactions'] = 0 
     580        if use_transactional_fixtures? 
     581          if Thread.current['open_transactions'] != 0 
     582            ActiveRecord::Base.connection.rollback_db_transaction 
     583            Thread.current['open_transactions'] = 0 
     584          end 
     585          Thread.current['transactional_test'] = false 
    582586        end 
    583587        ActiveRecord::Base.verify_active_connections! 
    584588      end 
  • a/activerecord/lib/active_record/transactions.rb

    old new  
    6969    # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you 
    7070    # should be ready to catch those in your application code. 
    7171    module ClassMethods 
    72       def transaction(&block) 
     72      def transaction(options={}, &block) 
    7373        previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" } 
    7474        increment_open_transactions 
    7575 
    7676        begin 
    77           connection.transaction(Thread.current['start_db_transaction'], &block) 
     77          connection.transaction((options[:force] == true) || Thread.current['start_db_transaction'], Thread.current['open_transactions'], &block) 
    7878        ensure 
    7979          decrement_open_transactions 
    8080          trap('TERM', previous_handler) 
    8181        end 
    8282      end 
    8383 
     84      # Sets the options for implicit transactions. For different 
     85      # action types. 
     86      # 
     87      # The action types are: 
     88      # * <tt>:save</tt> - transaction type for creating or updating a record. 
     89      # * <tt>:destroy</tt> - transaction type for deleting a record. 
     90      # 
     91      # The transaction types are: 
     92      # * <tt>:none</tt> - no transaction is created. 
     93      # * <tt>:flat</tt> - transaction is only created if non exist. This is the default. 
     94      # * <tt>:nested</tt> - transaction is created even if one exists. This only works if the database supports nested transactions, if it does not then the behaviour is the same as for :flat. 
     95      # 
     96      # Option examples: 
     97      #   set_transaction_types :save => :flat 
     98      #   set_transaction_types :save => :none, :destroy => :nested 
     99      #   set_transaction_types :nested 
     100      def set_transaction_types(options) 
     101        case options 
     102        when Symbol 
     103          options = { :save => options, :destroy => options } 
     104        when Hash 
     105          options[:save] ||= :flat 
     106          options[:destroy] ||= :flat 
     107        else 
     108          raise(ArgumentError, "Invalid transaction type(s): %s", options.inspect) 
     109        end 
     110 
     111        options.assert_valid_keys(:save, :destroy) 
     112 
     113        write_inheritable_attribute("transaction_types", options) 
     114      end 
     115 
     116      def get_transaction_type(action_type) 
     117        get_transaction_types[action_type] || :flat 
     118      end 
     119 
     120      def get_transaction_types 
     121        (read_inheritable_attribute("transaction_types") or write_inheritable_attribute("transaction_types", {})) 
     122      end 
     123 
    84124      private 
    85125        def increment_open_transactions #:nodoc: 
    86126          open = Thread.current['open_transactions'] ||= 0 
     
    93133        end 
    94134    end 
    95135 
    96     def transaction(&block) 
    97       self.class.transaction(&block) 
     136    def transaction(options={}, &block) 
     137      self.class.transaction(options, &block) 
    98138    end 
    99139 
    100140    def destroy_with_transactions #:nodoc: 
    101       transaction { destroy_without_transactions } 
     141      transaction_type = self.class.get_transaction_type(:destroy) 
     142      if transaction_type == :none 
     143        destroy_without_transactions 
     144      else 
     145        options = { :force => (transaction_type == :nested) } 
     146        transaction(options) { destroy_without_transactions } 
     147      end 
    102148    end 
    103149 
    104150    def save_with_transactions(perform_validation = true) #:nodoc: 
    105       rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } } 
     151      rollback_active_record_state! do 
     152        transaction_type = self.class.get_transaction_type(:save) 
     153        if transaction_type == :none 
     154          save_without_transactions 
     155        else 
     156          options = { :force => (transaction_type == :nested) } 
     157          transaction(options) { save_without_transactions(perform_validation) } 
     158        end 
     159      end 
    106160    end 
    107161 
    108162    def save_with_transactions! #:nodoc: 
    109       rollback_active_record_state! { transaction { save_without_transactions! } } 
     163      rollback_active_record_state! do 
     164        transaction_type = self.class.get_transaction_type(:save) 
     165        if transaction_type == :none 
     166          save_without_transactions! 
     167        else 
     168          options = { :force => (transaction_type == :nested) } 
     169          transaction(options) { save_without_transactions! } 
     170        end 
     171      end 
    110172    end 
    111173 
    112174    # Reset id and @new_record if the transaction rolls back. 
  • a/activerecord/test/abstract_unit.rb

    old new  
    6363ActiveRecord::Base.connection.class.class_eval do   
    6464   
    6565  if not (const_get('IGNORED_SQL') rescue nil)     
    66     IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/
     66    IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SHOW FIELDS/
    6767 
    6868    def execute_with_counting(sql, name = nil, &block) 
    6969      $query_count ||= 0 
  • a/activerecord/test/transactions_test.rb

    old new  
    279279    end 
    280280  end 
    281281end 
     282 
     283if current_adapter?(:PostgreSQLAdapter) or current_adapter?(:MysqlAdapter) 
     284  class NestedTransactionsTest < TransactionTest 
     285 
     286    def test_nested_explicit_transactions_with_forced_nesting 
     287      Topic.transaction do 
     288        @first.approved = true 
     289        @first.save 
     290        @second.approved = true 
     291        @second.save 
     292        begin 
     293          Topic.transaction :force => true do 
     294            @second.approved = false 
     295            @second.save 
     296            raise "Bad things!" 
     297          end 
     298        rescue 
     299          # ignore the exception 
     300        end 
     301      end 
     302 
     303      assert Topic.find(1).approved?, "First should have been approved" 
     304      assert Topic.find(2).approved?, "Second should have been approved" 
     305    end 
     306 
     307    def test_nested_explicit_transactions_without_forced_nesting 
     308      Topic.transaction do 
     309        @first.approved = true 
     310        @first.save 
     311        @second.approved = true 
     312        @second.save 
     313        begin 
     314          Topic.transaction do 
     315            @second.approved = false 
     316            @second.save 
     317            raise "Bad things!" 
     318          end 
     319        rescue 
     320          # ignore the exception 
     321        end 
     322      end 
     323 
     324      assert Topic.find(1).approved?, "First should have been approved" 
     325      assert !Topic.find(2).approved?, "Second should have been unapproved" 
     326    end 
     327 
     328    def test_transaction_type_save_none 
     329      with_topic_transaction_options :save => :none do 
     330        assert_queries(1) { @first.save! } 
     331      end 
     332    end 
     333 
     334    def test_transaction_type_save_flat 
     335      with_topic_transaction_options :save => :flat do 
     336        assert_queries(3) { @first.save! } 
     337      end 
     338    end 
     339 
     340    def test_transaction_type_save_flat_inside_transaction 
     341      with_topic_transaction_options :save => :flat do 
     342        Topic.transaction do 
     343          assert_queries(1) { @first.save! } 
     344        end 
     345      end 
     346    end 
     347 
     348    def test_transaction_type_save_nested 
     349      with_topic_transaction_options :save => :nested do 
     350        assert_queries(3) { @first.save! } 
     351      end 
     352    end 
     353 
     354    def test_transaction_type_save_nested_inside_transaction 
     355      with_topic_transaction_options :save => :nested do 
     356        Topic.transaction do 
     357          assert_queries(3) { @first.save! } 
     358        end 
     359      end 
     360    end 
     361 
     362    def test_transaction_type_destroy_none 
     363      with_topic_transaction_options :destroy => :none do 
     364        assert_queries(8) { assert @first.destroy } 
     365      end 
     366    end 
     367 
     368    def test_transaction_type_destroy_flat 
     369      with_topic_transaction_options :destroy => :flat do 
     370        assert_queries(10) { assert @first.destroy } 
     371      end 
     372    end 
     373 
     374    def test_transaction_type_destroy_nested 
     375      with_topic_transaction_options :destroy => :nested do 
     376        assert_queries(12) { assert @first.destroy } 
     377      end 
     378    end 
     379 
     380    private 
     381      def with_topic_transaction_options(options) 
     382        # NOTE Reply does not inherit this because the class 
     383        # has already been loaded 
     384        old_types_topic = Topic.send(:get_transaction_types) 
     385        old_types_reply = Topic.send(:get_transaction_types) 
     386 
     387        begin 
     388          Topic.send(:set_transaction_types, options) 
     389          Reply.send(:set_transaction_types, options) 
     390          yield 
     391        ensure 
     392          Topic.send(:set_transaction_types, old_types_topic) 
     393          Reply.send(:set_transaction_types, old_types_reply) 
     394        end 
     395      end 
     396  end 
     397end