よく忘れるので、ここにメモします。
戻り値
ブロックの戻り値がtransaction
メソッドの戻り値になります。
val = ActiveRecord::Base.transaction do # BEGIN
user.save!
true
end # COMMIT
val #=> true
例外でROLLBACK
ブロック中で例外が発生すると、ROLLBACKが行われ、例外が再送出されます。
ActiveRecord::Base.transaction do # BEGIN
user1.save! # 実行される(ROLLBACKで戻る)
raise "foo" # ROLLBACK
user2.save! # 実行されない
end
do_something # 実行されない
例外 ActiveRecord::Rollback
ただし、例外がActiveRecord::Rollback
の場合は、ROLLBACKが行われたあとで、例外は再送出されず、transaction
メソッドが完了します。この場合の戻り値はnil
になります。
ActiveRecord::Base.transaction do # BEGIN
user1.save! # 実行される(ROLLBACKで戻る)
raise ActiveRecord::Rollback # ROLLBACK
user2.save! # 実行されない
end
do_something # 実行される
行ロックを使う
行ロック(SELECT FOR UPDATE)を使いたい場合は、ブロック中でlock
メソッドを使ってレコードを取り出します。前に実行中のトランザクションがあれば、lock
メソッドのところで待ち状態になります。COMMITまたはROLLBACKでロックが解除され、待ち状態だったトランザクションが再開されます。
ActiveRecord::Base.transaction do # BEGIN
# SELECT ... FOR UPDATE
# 前に実行中のトランザクションがあればロック解除待ち、
# なければクエリー実行
user = User.lock.find(123)
user.save!
end # COMMIT、ロック解除
行ロックを使い、ほかの読み込みを待たせる
SELECT FOR UPDATEはトランザクションの外でも使えます。あるレコードの保存作業中にはそのレコードを読み込みさせたくない、というときに使えます。Aの開始直後にBが実行されると、AがコミットされるまでBのfindは待ち続けます(PostgreSQLとMySQLで確認済み)。
A.レコードを保存しようとしているタスク
ActiveRecord::Base.transaction do # BEGIN
user = User.lock.find(123) # SELECT ... FOR UPDATE
user.save!
end # COMMIT、ロック解除
B.レコードを読もうとしているタスク
user = User.lock.find(123) # SELECT ... FOR UPDATE
(ただし、この挙動はautocommitフラグなど条件によって変わるもよう。いつか調べたい。)