18858


Railsに限らず、WebのMVCなら書き間違えてしまいかねないのですが、

### 本来の形

本来下記のようにすべきところを

[code]

    def hoge
      ActiveRecord::Base.transaction do
        ....
        ....   
        raise ActiveRecord::RecordNotDestroyed unless Foo.destroy_all
      end
      redirect_to .....
    rescue ActiveRecord::RecordNotDestroyed
      redirect_to .....
    end
    
[/code]

### 誤り

遷移処理をトランザクションの中に入れてしまった場合に具体的な問題は起こりますでしょうか？

[code]

    def hoge
      ActiveRecord::Base.transaction do
        ....
        ....   
        raise ActiveRecord::RecordNotDestroyed unless Foo.destroy_all
        redirect_to ..... # <- トランザクション中に書いてしまった！
      end
    rescue ActiveRecord::RecordNotDestroyed
      redirect_to .....
    end
    
[/code]

### 説明

`raise`で例外が起こった場合はどちらも `rescue`
に処理が飛ぶので、例外が起こったとしても処理の流れに違いはないように思います。（とはいえ、DBに関するブロックに遷移処理が入っているので、ロジック的にもソースコードの美しさ的にも変だとは思いますが）


CC BY-SA 3.0
2015-11-14T08:50:47.477
18810
2015-11-16T04:11:28.950
2015-11-14T09:23:28.263
9008
9008
question
ruby-on-rails ruby mvc database
トランザクションの中で遷移処理を書いてしまうと何か具体的な問題は発生しますか？


この例だけで言うと、実際に試してみてトランザクションが正常にコミットできていれば、副次的な問題が起きる可能性は低いと思います。

以下は余談ですが、例外以外の要因でトランザクションを阻害するケースを考えてみました。  
問題が起きる可能性がああるのは`do`ブロック内で`return`してしまった場合が考えられます。  
ブロック内での`return`はブロックを呼び出している`yield`の終了だけでなく、ブロックの呼び出し元の終了も行うためです。

[code]

    -- hoge.rb -- 
    def aaa
      yield
      p 123
    end
    
    def hoge
      aaa do
        p 'aiueo'
        return 200
      end
      100
    end
    
    ret = hoge
    p ret
    
[/code]

上記のスクリプトを実行すると以下のようになります。

[code]

    $ruby hoge.rb
    "aiueo"
    200 
    # "123"の出力もないし、戻り値が100でなくてブロック内でreturnした200になってる
    
[/code]

この例から分るようにブロック内の`return`は結構混乱要因です。`ActiveRecord::Base.transaction`の実装次第では危険があります。

[code]

    begin
      yield
    ensure
      コミット
    end
    
[/code]

という実装であればブロック内で`return`されようが必ずコミットはされるでしょうが、例外が出たらロールバックすることを考えると、

[code]

    begin
     yield
     コミット
    rescue Exception=> e
      ロールバック
    end
    
[/code]

という実装である可能性もあるかもしれません。  
後者の実装の場合、`ActiveRecord::Base.transaction`の`do`ブロックで`return`を使うと、まずコミットされないままトランザクションだけは継続し、同じく`do`ブロック呼び出し元のコントローラメソッドもおそらく即終了となるため、何らかのDB例外につながると推測できそうです。

* * *

**以下追記**  
`ActiveRecord::Base.transaction`のソースコードをざっと見てみたところ、  
`ConnectionAdapters::DatabaseStatements#transaction`に定義がされています。

[code]

    def transaction(options = {})
      options.assert_valid_keys :requires_new, :joinable, :isolation
      if !options[:requires_new] && current_transaction.joinable?
        if options[:isolation]
          raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
        end
        yield
      else
        transaction_manager.within_new_transaction(options) { yield }
      end
    rescue ActiveRecord::Rollback
      # rollbacks are silently swallowed
    end
    
[/code]

これだけを見ると、yieldで呼び出されたdoブロックでreturnが使われると果たして動作がどうなるのか分からない感じがします。そもそもこのyieldに到達するための条件すらよく分かりませんが、とにかくこのパスを通過する場合に限っては、`do`ブロック内の`return`は危ないかもしれません。

それ以外の場合は`ActiveRecord::ConnectionAdapters::TransactionState`クラスの`#within_new_transaction`が処理を行うようです。

[code]

    def within_new_transaction(options = {})
      transaction = begin_transaction options
      yield
    rescue Exception => error
      rollback_transaction if transaction
      raise
    ensure
      unless error
        if Thread.current.status == 'aborting'
        rollback_transaction if transaction
      else
        begin
          commit_transaction
        rescue Exception
          transaction.rollback unless transaction.state.completed?
          raise
        end
      end
    end
    
[/code]

これを見る限り、`ActiveRecord`は`yield`が`return`で抜けられても、その後`ensure`ブロックにてエラーでチェックを行い、正常ならコミットをしてるっぽいです。  
このような実装なので`ActiveRecord`に関しては、一部不明な点がありますが、多分、大体、`return`を使っても安全そうです。


CC BY-SA 3.0
2015-11-15T20:08:26.130
18858
2015-11-16T04:11:28.950
2015-11-16T04:11:28.950
9403
9403
18810
answer