问题 如何在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语句。如何在这种情况下禁用转换(不在整个应用程序中禁用它们)?


13038
2018-03-16 11:32


起源

#删除事务ActiveRecord :: ConnectionAdapters :: MysqlAdapter.class_eval do def begin_db_transaction end def commit_db_transaction end end - Kanti
我可能在某处错过了一点,但这是一个交易的问题是什么? - Almaron
@Kanti您的解决方案将禁用交易 整个 应用。 - Maxim Dobryakov
@Almaron INSERT是原子操作。 - Maxim Dobryakov
@maxd没有看到问题。 - Almaron


答案:


怎么运行的:

持久性模块定义 createhttps://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

此时事务将save(super)包裹起来,再次位于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上的方法创建!并保存!在模块验证中。在Rails 4.2上有Persistence。

编辑:也许你认为你可以赚取交易所用的时间。在我的例子中,提交时间转到插入(我有一个标准的HD,我认为你有一个SSD)。


8
2018-03-22 04:11



这是一个很好的解释。从我的角度来看,删除插入/更新查询的事务有两个原因:1)它不应该存在,因为它是不必要的,2)它加速整个系统。是的,不是那么多,但如果你每分钟处理数百个请求,那将是一个很好的奖励。无论如何,它可能是ActiveRecord的潜在PR - Alexander
@Alexander非常感谢!我认为这是一个学习挑战,但我不认为对PR有PR,因为我知道失去交易回调的风险我不能坚持它,而且我们没有基准来显示任何提高的速度。 - Alejandro Babio


这里的问题是您想要修改类级方法的行为。这本质上不是线程安全的,至少对于其他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! }

2
2018-03-22 02:17



你的第一个建议似乎对Rails 5没有任何影响 - akostadinov
@akostadinov令人惊讶,因为交易机制似乎没有太大变化。你有任何代码和SQL日志来证明这一点吗? - PinnyM
我只是从上面复制/粘贴您的模型代码但使用了 Model.save skip_transaction: true。不创造。我确定它被调用了,因为起初我忘记了 attr_accessor :skip_transaction 并且它提出不存在这样的局部变量或方法。我的应用程序在这里,但没有变形 github.com/akostadinov/ownthat - akostadinov


最简单的方法是手动编写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)

不像上面使用亚历杭德罗的解决方案那么好,但是做到了 - 特别是如果它是一次性的,桌子不太可能改变。


1
2018-03-23 04:40





我不知道这样做的好方法

在ruby 2.2上你可以做到

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

这不适用于红宝石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

要自动走向链,直到找到事务位,但不知道你可能跳过了什么代码。


0
2018-03-16 11:53



修补猴子 save 或其他方法不是解决方案。它可以打破所有ActiveRecord堆栈! - Maxim Dobryakov
确实可以 - 这就是为什么我说我不推荐它 - Frederick Cheung


Alejandro Babio的答案很广泛但想要解释为什么交易首先完成。

这个答案 解释了交易在通话中的作用。简而言之就是这样:

begin transaction
insert record
after_save called
commit transaction
after_commit called

但没有提供 after_save hook是由开发人员注册的,我想知道为什么不跳过事务。对于高延迟连接,事务可能会将总体操作时间增加3倍:/ IMO Rails需要进行优化。

Rails拒绝了这样的优化,看看为什么: https://github.com/rails/rails/issues/26272


0
2017-08-24 15:10