Как пропустить транзакцию в ActiveRecord для инструкции INSERT ONLY?

Посмотрите на этот пример:

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">

Как вы можете видеть, метод create! выполняет оператор insert внутри бесполезной транзакции. Как отключить трансацию только в этом случае (без отключения их в целом приложении)?

Ответ 1

Как это работает:

Модуль сохранения определяет 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

Создает объект и вызывает #save!

Он не задокументирован в общедоступном api, но вызывает 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

В этот момент транзакция завершает сохранение (супер), которое снова находится в модуле Persistence: 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

Позвольте взломать это с помощью некоторых новых методов:

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

Возможно, вы хотите использовать стандартный метод create!, тогда вам нужно переопределить его. Я определяю первый необязательный параметр :atomic, а когда он присутствует, вы хотите использовать метод atomic_save!.

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

С этим в config/initializers/<any_name>.rb он может работать.

Как он работает на консоли:

~/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
}

Предостережение: вы потеряете after_rollback и after_commit обратные вызовы!

Примечание: в 4.1 методы создаются! и сохранить! находятся в модуле Validations. На Rails 4.2 находятся в Persistence.

Изменить. Возможно, вы считаете, что можете заработать истекшее время транзакции. В моих примерах время фиксации идет на вставки (у меня стандартный HD, и я думаю, что у вас SSD).

Ответ 2

Проблема заключается в том, что вы хотите изменить поведение метода класса. Это по своей сути не является потокобезопасным, по крайней мере, для одновременных транзакций для других объектов Stat. Простым обходным решением было бы отметить экземпляр как не требующий транзакции:

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

Если вы работаете в одной файловой системе и поэтому не занимаетесь приостановкой транзакций для объектов Stat в течение этого времени, вы можете использовать методы уровня класса и завернуть вызов следующим образом:

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

Самый простой способ - вручную написать инструкцию INSERT, все еще используя ActiveRecord для ее выполнения. Это не будет отключать транзакции для любого другого кода, который вы пишете.

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)

Не так хорошо, как использовать решение Alejandro выше, но делает трюк - особенно если он отключен, и таблица вряд ли изменится.

Ответ 4

Я не знаю, какой хороший способ сделать это

В рубине 2.2 вы можете сделать

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

Это не будет работать до ruby ​​2.2 (что при добавлении super_method) и работает только потому, что в списке предков транзакции являются первыми (или последними, в зависимости от того, каким образом вы заказываете), чтобы переопределить сохранение. Если бы это было не так, этот код пропустил бы "неправильный" метод сохранения. Таким образом, я вряд ли мог бы рекомендовать этот

Вы можете сделать что-то вроде

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

Чтобы автоматически перейти по цепочке до тех пор, пока вы не найдете бит транзакции, но не знаете, какой код вы могли бы пропустить.

Ответ 5

Ответ Алехандро Бабио обширен, но он хотел объяснить, почему транзакция выполняется в первую очередь.

Этот ответ объясняет, какую роль играет транзакция в вызове. Вот он вкратце:

begin transaction
insert record
after_save called
commit transaction
after_commit called

Но если разработчик не зарегистрировал крючок after_save, мне интересно, почему транзакция не пропущена. Для соединений с высокой задержкой транзакция может увеличить общее время работы 3 раза:/IMO Rails необходимо оптимизировать.

Rails отклонили такую ​​оптимизацию, посмотрите, почему: https://github.com/rails/rails/issues/26272