单个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>


调用栈如下

ar_save.html


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如下

ar_transaction.html

先定位到两个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>


调用栈如下

ar_rollback.html

虽然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