【问题标题】:How to skip transaction in ActiveRecord for INSERT ONLY statement?如何在 ActiveRecord 中为 INSERT ONLY 语句跳过事务?
【发布时间】:2015-03-16 11:32:00
【问题描述】:

看这个例子:

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! 方法在无用事务中执行插入语句。如何仅在这种情况下禁用转换(而不在整个应用程序中禁用它们)?

【问题讨论】:

  • # 删除事务 ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do def begin_db_transaction end def commit_db_transaction end end
  • 我可能在某处遗漏了一点,但作为交易有什么问题?
  • @Kanti 您的解决方案将禁用整个应用程序中的事务。
  • @Almaron INSERT 是原子操作。
  • @maxd 没有发现问题。

标签: ruby-on-rails ruby-on-rails-4 activerecord


【解决方案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

此时事务包装保存(超级),它再次在持久性模块: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/&lt;any_name&gt;.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 上,方法创建!并保存!在模块验证中。 On Rails 4.2 处于 Persistence 中。

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

【讨论】:

  • 这是一个很好的解释。从我的角度来看,删除插入/更新查询的事务有两个原因:1)它不应该存在,因为它是不必要的;2)它加速了整个系统。是的,不是那么多,但如果您每分钟处理数百个请求,那将是一个不错的奖励。无论如何,它可能是 ActiveRecord 的潜在 PR
  • @Alexander 非常感谢!我认为这是一个学习挑战,但我不认为 PR 到 AR,因为我无法支持它知道丢失交易回调的风险,而且我们没有显示任何改进速度的基准。跨度>
【解决方案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! }

【讨论】:

  • 您的第一个建议似乎对 Rails 5 没有任何影响
  • @akostadinov 这令人惊讶,因为事务机制似乎没有太大变化。你有任何代码和 SQL 日志来证明这一点吗?
  • 我只是从上面复制/粘贴了您的模型代码,但使用了Model.save skip_transaction: true。不创建。我确定它被调用是因为起初我忘记了attr_accessor :skip_transaction,并且它提出不存在这样的本地变量或方法。我的应用程序在这里,但没有 chenge github.com/akostadinov/ownthat
  • 在 Rails 5.2 上运行良好。我只用yield 替换了with_transaction_returning_status 的主体以防止事务(该特定表不受事务控制,尝试事务将引发SQL7008(DB2 @ AS/400))并且它像宣传的那样工作。跨度>
【解决方案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】:

    我不知道有什么好方法

    在 ruby​​ 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
    

    自动沿着链向上走,直到您找到交易位,但不知道您可能跳过了哪些代码。

    【讨论】:

    • 猴子补丁save 或其他方法不是解决方案。它可以破坏所有 ActiveRecord 堆栈!
    • 确实可以——这就是为什么我说我不会推荐它
    【解决方案5】:

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

    This answer 解释了事务在调用中的作用。简而言之:

    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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-12-25
      • 2021-05-30
      • 1970-01-01
      • 1970-01-01
      • 2017-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多