看看这个例子:
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语句。如何在这种情况下禁用转换(不在整个应用程序中禁用它们)?
答案 0 :(得分:9)
工作原理:
持久性模块定义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上的方法创建!并保存!在模块验证中。在Rails 4.2上有Persistence。
修改:也许您认为可以获得交易已用时间。在我的例子中,提交时间转到插入(我有一个标准的HD,我认为你有一个SSD)。
答案 1 :(得分: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! }
答案 2 :(得分:1)
最简单的方法是手动编写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)
不如上面使用亚历杭德罗的解决方案那么好,但可以解决问题 - 特别是如果它一次性关闭并且表格不太可能改变。
答案 3 :(得分:0)
我不知道有什么好办法吗
在ruby 2.2上你可以做到
stat = Stat.new
stat.method(:save).super_method.call
这不会对pre 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
要自动走向链,直到找到事务位,但不知道你可能跳过了什么代码。
答案 4 :(得分:0)
亚历杭德罗·巴比奥的答案很广泛,但想要解释为什么交易首先完成。
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