こんにちは、三苫です。
ActiveRecordのトランザクションについてちょっとしたtipsです。
以下のActiveRecordを使ったrubyコードがどのようなクエリを発行するだろう?
1 2 3 4 5 6 7 8 9 |
Entity.transaction do Entity.transaction do Entity.create!(name: "hoge") end Entity.transaction do Entity.create!(name: "moge") raise ActiveRecord::Rollback end end |
おそらく多数の人が期待する動作はこう(以下、クエリ例はPostgreSQLのものです)
1 2 3 4 |
BEGIN INSERT INTO "entities" ("name") values ('hoge') RETURNING "id" INSERT INTO "entities" ("name") values ('moge') RETURNING "id" ROLLBACK -- ネストしたトランザクションが落ちたら、そりゃ全ロールバックでしょ~ |
実際の動作はこう!
1 2 3 4 |
BEGIN INSERT INTO "entities" ("name") values ('hoge') RETURNING "id" INSERT INTO "entities" ("name") values ('moge') RETURNING "id" COMMIT -- ←ロールバックやと言っとる範囲までコミットされとるやないか!!!! |
世間は厳しい!!
全ロールバックしたければ書くべきコードはこう!!!!
1 2 3 4 5 6 7 8 9 |
Entity.transaction do Entity.transaction do Entity.create!(name: "hoge") end Entity.transaction do Entity.create!(name: "moge") raise "Rollback!" # ネストしたトランザクションブロックを突き破るような例外を投げよう end end |
ネストしたトランザクションブロックだけロールバックしたければ書くべきコードはこう!
1 2 3 4 5 6 7 8 9 |
Entity.transaction do Entity.transaction do Entity.create!(name: "hoge") end Entity.transaction(requires_new: true) do # requires_new を指定しよう Entity.create!(name: "moge") raise ActiveRecord::Rollback end end |
その際のクエリはこう!
1 2 3 4 5 6 |
BEGIN INSERT INTO "entities" ("name") values ('hoge') RETURNING "id" SAVEPOINT active_record_1 -- SAVEPOINT を発行してくれる INSERT INTO "entities" ("name") values ('moge') RETURNING "id" ROLLBACK TO SAVEPOINT active_record_1 -- SAVEPOINT までロールバックしてくれる COMMIT -- めでたしめでたし。 |
では、このわかりにくい挙動、ドキュメンテーションされているのかどうか。
もちろんドキュメンテーションされています。ドキュメントを読まずに思い込みで実装してはいけない。
特に、トランザクションのようなセンシティブな問題は・・・。トランザクションが必要とされる処理はセンシティブ・・・。
ドキュメントは英語なので訳したものを以下に記載します。
上で書いたことがほぼそのまま書かれています。最初からドキュメントを読んでいれば・・・。
該当箇所は以下です。
https://github.com/rails/rails/blob/v4.2.0/activerecord/lib/active_record/transactions.rb#L142
訳が間違ってる可能性もあるので、微妙に感じた場合は是非原文を読みましょう。
Nested transactions
(ネストしたトランザクション)
+transaction+ calls can be nested. By default, this makes all database
statements in the nested transaction block become part of the parent
transaction. For example, the following behavior may be surprising:
トランザクションメソッドはネストして呼ぶことができる。
デフォルトでは、ネストしたトランザクション内でのステートメントはすべて親トランザクションの一部として実行される。
以下の例は、もしかしたら驚くかもしれない。(訳注:そら驚くやろ!)
1 2 3 4 5 6 7 |
User.transaction do User.create(username: 'Kotori') User.transaction do User.create(username: 'Nemu') raise ActiveRecord::Rollback end end |
creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
exception in the nested block does not issue a ROLLBACK. Since these exceptions
are captured in transaction blocks, the parent block does not see it and the
real transaction is committed.
上記の例ではKotoriとNemuが作成される。理由は ActiveRecord::Rollback はネストしたトランザクションブロックをロールバックしないからだ。
ActiveRecord::Rollbackのような例外はトランザクションブロックの中で捕捉され、親のトランザクションブロックからは検知できないし、実際のトランザクションはコミットされる。
In order to get a ROLLBACK for the nested transaction you may ask for a real
sub-transaction by passing <tt>requires_new: true</tt>. If anything goes wrong,
the database rolls back to the beginning of the sub-transaction without rolling
back the parent transaction. If we add it to the previous example:
ネストしたトランザクションをロールバックするためには、requires_new: true を指定して実際のサブトランザクションとしなければならない。
もし、何か良くないことが起きたときにサブトランザクションだけをロールバックし、親トランザクションをロールバックしたくないときは前の例を以下のように修正すればよい。
1 2 3 4 5 6 7 |
User.transaction do User.create(username: 'Kotori') User.transaction(requires_new: true) do User.create(username: 'Nemu') raise ActiveRecord::Rollback end end |
only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
Kotoriだけが作成される。これはMySQLとPostgreSQLで動作する。SQLite3では 3.6.8 以上でサポートされる。
Most databases don't support true nested transactions. At the time of
writing, the only database that we're aware of that supports true nested
transactions, is MS-SQL. Because of this, Active Record emulates nested
transactions by using savepoints on MySQL and PostgreSQL. See
http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
for more information about savepoints.
ほとんどのデータベースはネストしたトランザクションをサポートしない。
このドキュメントを書いている時点ではネストしたトランザクションをサポートしているのはMS-SQLだけだ。
なので、ActiveRecordはネストしたMySQLとPostgreSQLにおいてネストしたトランザクションをSAVEPOINTを利用してエミュレートしている。
SAVEPOINTについての情報はMySQLの以下のドキュメントに詳しい。
http://dev.mysql.com/doc/refman/5.6/en/savepoint.html
(訳注:日本語でPostgreSQLの場合はこちら参照 http://www.postgresql.jp/document/9.3/html/sql-savepoint.html )