非トランザクショナルリソースを含むトランザクション

たぶん5年以上に渡って、何度かコードレビューで話している気がするのでポインタを作っておく。

TL; DR

RDBMS の恩恵を受けられるトランザクショナルリソースと、ひとつの非トランザクショナルリソースでトランザクションを括る場合は、例外発生時にロールバック可能な RDBMS のリソース方を先に操作する。

もう少し書く

以下は雑に書いたサンプルコード。この foo メソッドは、トランザクション境界となるコントローラやバッチにあるメソッドを想定している。Alternative Rails な考えを持った設計の場合はサービスレイヤーに置かれるメソッドとなる。

def foo
  ActiveRecord::Base.transaction do
    current_user.update!(foo: params[:foo])

    Model.create!(bar: params[:bar])

    API.delete!(current_user.an_attribute)
  end
end

上記サンプルコードの current_userModelActiveRecord のモデル、つまり RDBMSトランザクションロールバックできるものとする。一方で API はここではロールバックできない API の呼び出しとする (ロールバック API を用意していればまた別の巻き戻し実装を考えられる) 。API に関してはロールバックできないファイルシステムなんかに置き換えて考えても良いかもしれない。

これら3つのメソッドを呼んだ際に例外が発生した際に、データをロールバックすることを期待したものとなる (実際に API での通信での例外の場合は、起きた例外によって処理を振り分ける必要があると思うのがここでは簡略化している) 。

(常にこの設計が正しいわけではなく、コンテキストにより設計が変わる例として、例外を発生させない非トランザクショナルリソースへの操作について、RDBMS リソースのコミットが成功した場合は常に後続の処理を行なうとして AR::Base.transaction の外に置くというアプローチもある。)

ポイントとしては3つ。

  1. AR::Base.transaction で囲む (基本)
  2. transaction 内のリソース更新では、失敗時に例外を起こす破壊的メソッドを (用意して) 呼び出す
  3. 先にロールバック可能な RDBMS のトランザクショナルリソースへの操作をしておいて、最後に失敗したら巻き戻しのできない非トランザクショナルリソースを操作する

順番を変えて current_user.update!API.delete!Mode.create! のの流れで考えると、最後の Model.create! の際に例外が起きたときに非トランザクショナルリソースの API.delete! に対してロールバックできないという問題が起きる。

補足の Pro tip としては、必要に応じた rescue による例外処理を入れるなどある。