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)?
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).
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! }
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.
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.
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