LoginSignup
30
17

More than 3 years have passed since last update.

RailsのtransactionメソッドとSQL

Last updated at Posted at 2019-08-06

よく忘れるので、ここにメモします。

戻り値

ブロックの戻り値が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フラグなど条件によって変わるもよう。いつか調べたい。)

30
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
17