How to skip transaction in ActiveRecord for INSERT ONLY statement?

匿名 (未验证) 提交于 2019-12-03 02:49:01

问题:

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:20:08', '2015-03-16 11:20:08')    (0.4ms)  COMMIT  => #<Stat id: 1, uid: nil, country: nil, city: nil, created_at: "2015-03-16 11:20:08", updated_at: "2015-03-16 11:20:08"> 

As you can see the create! method execute insert statement inside useless transaction. How to disable transation in this case only (without disabling them in whole application)?

回答1:

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).



回答2:

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! } 


回答3:

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.



回答4:

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.



回答5:

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



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!