13

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

Raj
  • 22,346
  • 14
  • 99
  • 142
Maxim
  • 9,701
  • 5
  • 60
  • 108
  • # Remove transactions ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do def begin_db_transaction end def commit_db_transaction end end – Kanti Mar 16 '15 at 11:43
  • 1
    I might be missing a point here somewhere, but what's the problem with it being a transaction? – Almaron Mar 16 '15 at 11:57
  • @Kanti Your solution will disable transactions in **whole** application. – Maxim Mar 16 '15 at 12:01
  • 1
    @Almaron INSERT is atomic operation. – Maxim Mar 16 '15 at 12:05
  • @maxd don't see a problem there. – Almaron Mar 16 '15 at 12:46
  • @maxd - As Almaron has implied, you aren't saving all that much by skipping the transaction (2 DB roundtrips to start and commit the transaction). The only case I can see this being an issue would be for very large quantities of INSERTs over a very brief period, at which point it's likely that ActiveRecord isn't the right tool for this. That said, it can be done - see below. – PinnyM Mar 22 '15 at 04:02
  • Why do you want to disable it for INSERT? – Vitaliy Yanchuk Mar 22 '15 at 22:17
  • My use case is to avoid those 2 unnecessary round trips to the db. It's not much but why not save a few milliseconds off of every request if it's possible. – psmith Jul 06 '21 at 03:19

5 Answers5

10

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

Alejandro Babio
  • 5,189
  • 17
  • 28
  • 1
    That's a great explanation. From my point of view, there is two reasons of removing transaction for insert/update queries: 1) it shouldn't be there, because it's unnecessary and 2) it speeds the entire system up. Yes, not so much, but if you're dealing with hundreds request per minute, it would be a nice bonus. Anyway, it could be a potential PR to ActiveRecord –  Mar 22 '15 at 12:32
  • 1
    @Alexander thanks a lot! I take this as a learning challenge, but I don't think on a PR to AR, because I'll can't uphold it knowing the risk of lose transaction callbacks, and we have no benchmarks that show any improved speed. – Alejandro Babio Mar 22 '15 at 22:41
5

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! }
PinnyM
  • 35,165
  • 3
  • 73
  • 81
  • Your first suggestion does not seem to have any effect on Rails 5 – akostadinov Aug 24 '16 at 14:38
  • @akostadinov that's surprising, since the transaction mechanism doesn't appear to have changed much. Do you have any code and SQL logs to demonstrate this? – PinnyM Aug 24 '16 at 16:32
  • I just copy/pasted your model code from above but used `Model.save skip_transaction: true`. Not create. I'm sure it was called because at first I forgot `attr_accessor :skip_transaction` and it raised that no such local variabel or method exists. My app is here but without the chenge https://github.com/akostadinov/ownthat/ – akostadinov Aug 24 '16 at 18:08
  • Works fine on Rails 5.2. I replaced the body of `with_transaction_returning_status` with just `yield` to prevent a transaction (that specific table is not under transaction control and attempting a transaction will raise a SQL7008 (DB2 @ AS/400)) and it works as advertised. – Confusion Aug 02 '19 at 14:22
2

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.

Rots
  • 5,506
  • 3
  • 43
  • 51
0

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.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
0

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

akostadinov
  • 17,364
  • 6
  • 77
  • 85