How to skip transaction in ActiveRecord for INSERT ONLY statement?

前端 未结 5 993
孤独总比滥情好
孤独总比滥情好 2020-12-30 05:19

Look at this example:

2.1.3 :001 > Stat.create!
   (0.1ms)  BEGIN
  SQL (0.3ms)  INSERT INTO `stats` (`created_at`, `updated_at`) VALUES (\'2015-03-16 11:         


        
相关标签:
5条回答
  • 2020-12-30 05:30

    The problem here is that you want to modify behavior for a class-level method. This is inherently not thread-safe, at the very least for concurrent transactions for other Stat objects. A simple workaround would be to flag the instance as not requiring a transaction:

    class Stat < ActiveRecord::Base
      attr_accessor :skip_transaction
    
      def with_transaction_returning_status
        if skip_transaction
          yield
        else
          super
        end
      end
    end
    
    Stat.create! skip_transaction: true
    

    If you are running on a single threaded framework, and therefore aren't concerned with suspending transactions for Stat objects during this time, you can use class level methods and wrap the call like so:

    class Stat < ActiveRecord::Base
      def self.transaction(*args)
        if @skip_transaction
          yield
        else
          super
        end
      end
    
      def self.skip_transaction
        begin
          @skip_transaction = true
          yield
        ensure
          @skip_transaction = nil
        end
      end
    end
    
    Stat.skip_transaction { Stat.create! }
    
    0 讨论(0)
  • 2020-12-30 05:33

    The simplest way is to manually write your INSERT statement, still using ActiveRecord to execute it. This won't disable transactions for any other code you write.

    sql = "INSERT INTO stats (created_at, updated_at) VALUES ('2015-03-16 11:20:08', '2015-03-16 11:20:08')"
    ActiveRecord::Base.connection.execute(sql)
    

    Not as nice as using Alejandro's solution above, but does the trick - especially if it's a once off and the table is unlikely to change.

    0 讨论(0)
  • 2020-12-30 05:46

    How it works:

    The persistence module define create: https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/persistence.rb#L46

    def create!(attributes = nil, &block)
      if attributes.is_a?(Array)
        attributes.collect { |attr| create!(attr, &block) }
      else
        object = new(attributes, &block)
        object.save!
        object
      end
    end
    

    It create an object and call #save!

    It is not documented in the public api, but calls https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/transactions.rb#L290

    def save!(*) #:nodoc:
      with_transaction_returning_status { super }
    end
    

    At this point the transaction wrap the save (super), which is at Persistence module again: https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/persistence.rb#L141

    def save!(*)
      create_or_update || raise(RecordNotSaved.new(nil, self))
    end
    

    Let's hack this with some new methods:

    module ActiveRecord
      module Persistence
        module ClassMethods
    
          def atomic_create!(attributes = nil, &block)
            if attributes.is_a?(Array)
              raise "An array of records can't be atomic"
            else
              object = new(attributes, &block)
              object.atomic_save!
              object
            end
          end
    
        end
    
        alias_method :atomic_save!, :save!
      end
    end
    
    module ActiveRecord
      module Transactions
    
        def atomic_save!(*)
          super
        end
    
      end
    end
    

    Perhaps you want to use the standard create! method, then you need to redefine it. I define a first optional parameter :atomic, and when it's present means you want to use the atomic_save! method.

    module ActiveRecord
      module Persistence
        module ClassMethods
    
          def create_with_atomic!(first = nil, second = nil, &block)
            attributes, atomic = second == nil ? [first, second] : [second, first]
            if attributes.is_a?(Array)
              create_without_atomic!(attributes, &block)
            else
              object = new(attributes, &block)
              atomic == :atomic ? object.atomic_save! : object.save!
              object
            end
          end
          alias_method_chain :create!, :atomic
    
        end
      end
    end
    

    With this in config/initializers/<any_name>.rb it can work.

    How it runs at console:

    ~/rails/r41example (development) > Product.atomic_create!(name: 'atomic_create')
      SQL (99.4ms)  INSERT INTO "products" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-03-22 03:50:07.558473"], ["name", "atomic_create"], ["updated_at", "2015-03-22 03:50:07.558473"]]
    => #<Product:0x000000083b1340> {
                :id => 1,
              :name => "atomic_create",
        :created_at => Sun, 22 Mar 2015 03:50:07 UTC +00:00,
        :updated_at => Sun, 22 Mar 2015 03:50:07 UTC +00:00
    }
    ~/rails/r41example (development) > Product.create!(name: 'create with commit')
      (0.1ms)  begin transaction
      SQL (0.1ms)  INSERT INTO "products" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-03-22 03:50:20.790566"], ["name", "create with commit"], ["updated_at", "2015-03-22 03:50:20.790566"]]
      (109.3ms)  commit transaction
    => #<Product:0x000000082f3138> {
                :id => 2,
              :name => "create with commit",
        :created_at => Sun, 22 Mar 2015 03:50:20 UTC +00:00,
        :updated_at => Sun, 22 Mar 2015 03:50:20 UTC +00:00
    }
    ~/rails/r41example (development) > Product.create!(:atomic, name: 'create! atomic')
      SQL (137.3ms)  INSERT INTO "products" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-03-22 03:51:03.001423"], ["name", "create! atomic"], ["updated_at", "2015-03-22 03:51:03.001423"]]
    => #<Product:0x000000082a0bb8> {
                :id => 3,
              :name => "create! atomic",
        :created_at => Sun, 22 Mar 2015 03:51:03 UTC +00:00,
        :updated_at => Sun, 22 Mar 2015 03:51:03 UTC +00:00
    }
    

    Caveat: You will lose after_rollback and after_commit callbacks!

    Note: on 4.1 the methods create! and save! are in module Validations. On Rails 4.2 are in Persistence.

    Edit: Perhaps you think you can earn the transaction elapsed time. In my examples the commit time goes to the inserts (I have a standard HD and I think you have an SSD).

    0 讨论(0)
  • 2020-12-30 05:53

    I don't know of any nice way of doing this

    On ruby 2.2 you can do

    stat = Stat.new
    stat.method(:save).super_method.call
    

    This won't work pre ruby 2.2 (that's when super_method was added) and only works because in the list of ancestors, transactions is the first (or last depending on which way you order) to override save. If it wasn't then this code would skip over the 'wrong' save method. As such, I could hardly recommend this

    You could do something like

    stat = Stat.new
    m = stat.method(:save)
    until m.owner == ActiveRecord::Transactions
      m = m.super_method
    end
    m = m.super_method
    

    To automatically walk up the chain until you have found the transactions bit, but there's no telling what code you might have skipped over.

    0 讨论(0)
  • 2020-12-30 05:54

    Answer of Alejandro Babio is extensive but wanted to explain why transaction is done in the first place.

    This answer explains what role does the transaction have in the call. Here is it in short:

    begin transaction
    insert record
    after_save called
    commit transaction
    after_commit called
    

    But provided no after_save hook is registered by developer, I wonder why transaction is not skipped. For high latency connections, the transaction may increase overall operation time 3 times :/ IMO Rails needs to be optimized.

    Rails rejected such optimization, see why: https://github.com/rails/rails/issues/26272

    0 讨论(0)
提交回复
热议问题