Active Record Transactions
单个save
irb(main):003:0> ar = Article.new(title: 'ttttt', text: 'gfjgdjg') => #
<article id:="" nil,="" title:="" "ttttt",="" text:="" "gfjgdjg",="" created_at:="" nil,="" updated_at:="" nil="">irb(main):004:0> binding.trace_tree(html: true, tmp: ['rails', 'ar_save.html']){ar.save} (1450.6ms) begin transaction SQL (4751.9ms) INSERT INTO "articles" ("title", "text", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "ttttt"], ["text", "gfjgdjg"], ["created_at", 2017-03-22 13:23:19 UTC], ["updated_at", 2017-03-22 13:23:19 UTC]] (2018.3ms) commit transaction => true
</article>
调用栈如下
Transaction的save方法会将父类的save置于rollback_active_record_state!和with_transaction_returning_status中运行
def save(*) #:nodoc:
rollback_active_record_state! do
with_transaction_returning_status { super }
end
end
rollback_active_record_state!会在首尾执行remember_transaction_record_state和clear_transaction_record_state
# Save the new record state and id of a record so it can be restored later if a transaction fails.
def remember_transaction_record_state #:nodoc:
@_start_transaction_state[:id] = id
@_start_transaction_state.reverse_merge!(
new_record: @new_record,
destroyed: @destroyed,
frozen?: frozen?,
)
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
end
# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc:
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
end
# Force to clear the transaction record state.
def force_clear_transaction_record_state #:nodoc:
@_start_transaction_state.clear
end
此处调用栈如下

中间再调用with_transaction_returning_status { super }

joinaable一般来说是true

# activerecord-5.0.2/lib/active_record/transactions.rb
def transaction(options = {}, &block)
connection.transaction(options, &block)
end
# activerecord-5.0.2/lib/active_record/connection_adapters/abstract/database_statements.rb
def transaction(requires_new: nil, isolation: nil, joinable: true)
if !requires_new && current_transaction.joinable?
if isolation
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
yield
else
transaction_manager.within_new_transaction(isolation: isolation, joinable: joinable) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
connection会持有一个TransactionManager,一些transaction的检查和执行都是委托到它之上
delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
(connection 的获取等下再看)
首次发起transaction,都肯定是进入transaction_manager.within_new_transaction的,因为第一次调current_transaction,会返回joinable?为false的NullTransaction,属于一种null object pattern吧
class NullTransaction #:nodoc:
def initialize; end
def state; end
def closed?; true; end
def open?; false; end
def joinable?; false; end
def add_record(record); end
end
class TransactionManager
def current_transaction
@stack.last || NULL_TRANSACTION
end
private
NULL_TRANSACTION = NullTransaction.new
而如果是AR.transaction{ar1.save;ar2.save}形式,里面的两个save都会获取到@stack.last,而这个last transaction是joinable的,于是会直接执行with_transaction_returning_status
否则,用within_new_transaction来执行。它这里会先做begin_transaction,最后根据db返回来决定commit_transaction还是rollback_transaction
def within_new_transaction(options = {})
transaction = begin_transaction options
yield
rescue Exception => error
if transaction
rollback_transaction
after_failure_actions(transaction, error)
end
raise
ensure
unless error
if Thread.current.status == 'aborting'
rollback_transaction if transaction
else
begin
commit_transaction
rescue Exception
rollback_transaction(transaction) unless transaction.state.completed?
raise
end
end
end
end
此处对于AR.transaction{ar1.save;ar2.save}的情况,应该是里面两个save都会内嵌在begin_transaction后的yield中,然后进入到刚才if !requires_new && current_transaction.joinable?的第一个条件分支里,等下会再trace验证一下
begin_transaction会产生两种可能的Transaction,塞入栈中,对于单条save或非嵌套的AR.transaction{},应该都只会产生一个Transaction,即RealTransaction
def begin_transaction(options = {})
run_commit_callbacks = !current_transaction.joinable?
transaction =
if @stack.empty?
RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks)
else
SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options,
run_commit_callbacks: run_commit_callbacks)
end
@stack.push(transaction)
transaction
end
而对于AR.transaction{ar1.save;ar2.save}的情况又是怎样呢,现在试试,同时保存两个article(虽然现实中好像没有这种案例)
transaction包含两个save
irb(main):002:0> ar1 = Article.new(title: 'aaaaa', text: 'a') => #
<article id:="" nil,="" title:="" "aaaaa",="" text:="" "a",="" created_at:="" nil,="" updated_at:="" nil="">irb(main):003:0> ar2 = Article.new(title: 'bbbbb', text: 'b') => #
<article id:="" nil,="" title:="" "bbbbb",="" text:="" "b",="" created_at:="" nil,="" updated_at:="" nil="">irb(main):004:0> binding.trace_tree(html: true, tmp: ['rails', 'ar_transaction.html']){Article.transaction{ar1.save; ar2.save}} (2714.4ms) begin transaction SQL (6221.2ms) INSERT INTO "articles" ("title", "text", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "aaaaa"], ["text", "a"], ["created_at", 2017-03-23 03:01:10 UTC], ["updated_at", 2017-03-23 03:01:10 UTC]] SQL (7359.6ms) INSERT INTO "articles" ("title", "text", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "bbbbb"], ["text", "b"], ["created_at", 2017-03-23 03:04:37 UTC], ["updated_at", 2017-03-23 03:04:37 UTC]] (2550.4ms) commit transaction => true
</article></article>
完整trace如下
先定位到两个save在哪里

可以看到两个save如期在block (2 levels)中出现,并且with_transaction_returning_status没有再包进一层within_new_transaction里执行,而是直接yield,即是进入到if !requires_new && current_transaction.joinable?的第一个分支里。当两个save都执行完之后,才commit_transaction

既然这里三条命令(Article.transaction,ar1.save,ar2.save)都从同一个transaction stack取得transaction,即是他们应该都共享同一个TransactionManager,而connection又持有TransactionManager,所以connection应该有共享机制
connection共享
简单起见,只看看单条save如何抓connection

总体过程如上,基本上是三步:获取connection_handler、获取connection_pool、获取connection,详细如下



其中,connection_handler通过ActiveRecord::RuntimeRegistry获取,它extend了ActiveSupport::PerThreadRegistry,即是用来按Thread.current获取,而connection_pool则是通过Process.pid获取,connection又是通过Thread.current获取(至于为什么又用Process.pid又用Thread.current,迟点再研究),总之,对于同一执行流程的命令,connection是共享的
validate与rollback无关
设Article的title valadate长度至少为5
irb(main):014:0> ar4 = Article.new(title: 'd', text: 'd') => #
<article id:="" nil,="" title:="" "d",="" text:="" "d",="" created_at:="" nil,="" updated_at:="" nil="">irb(main):015:0> binding.trace_tree(html: true, tmp: ['rails', 'ar_rollback.html']){ar4.save} (2718.2ms) begin transaction (3611.0ms) rollback transaction => false irb(main):016:0> ar4 => #
<article id:="" nil,="" title:="" "d",="" text:="" "d",="" created_at:="" nil,="" updated_at:="" nil="">irb(main):018:0> ar4.errors => #<activemodel::errors:0x007f68a0306708 @base="#<Article" id:="" nil,="" title:="" "d",="" text:="" "d",="" created_at:="" nil,="" updated_at:="" nil="">, @messages={:title=>["is too short (minimum is 5 characters)"]}, @details={:title=>[{:error=>:too_short, :count=>5}]}> irb(main):019:0></activemodel::errors:0x007f68a0306708>
</article></article>
调用栈如下
虽然with_transaction_returning_status里有rescue ActiveRecord::Rollback,但明显这里抛的不是ActiveRecord::Rollback

因此,如果在validate阶段报错,是不会影响整个transaction的。如下,ar5依然被保存
irb(main):019:0> ar5 = Article.new(title: 'eeeee', text: 'e') => #
<article id:="" nil,="" title:="" "eeeee",="" text:="" "e",="" created_at:="" nil,="" updated_at:="" nil="">irb(main):020:0> Article.transaction{ar4.save;ar5.save} (0.1ms) begin transaction SQL (0.3ms) INSERT INTO "articles" ("title", "text", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "eeeee"], ["text", "e"], ["created_at", 2017-03-23 09:18:28 UTC], ["updated_at", 2017-03-23 09:18:28 UTC]] (5.4ms) commit transaction => true
</article>
你只能自己判断前一步有没有成功,或者强行save!
irb(main):023:0> Article.transaction{ar4.save and ar6.save} (0.1ms) begin transaction (0.1ms) commit transaction => false irb(main):024:0> Article.transaction{ar4.save!; ar6.save!} (1.7ms) begin transaction (0.1ms) rollback transaction ActiveRecord::RecordInvalid: Validation failed: Title is too short (minimum is 5 characters) #... irb(main):025:0> ar6 => #
<article id:="" nil,="" title:="" "fffff",="" text:="" "f",="" created_at:="" nil,="" updated_at:="" nil=""></article>
现实中,如果两个对象是很相关的,则通常会有association,这样validate会自动中止association的save