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