#はじめに
はじめまして。
railsを使用した開発を行っている実務経験1ヶ月のひよっこです。
業務の中でトランザクションの処理の実装を担当させていただく機会がありましたので復習のために投稿させていただきます。
下記投稿の内容は
http://markdaggett.com/blog/2011/12/01/transactions-in-rails/
という内容を一部和訳したものとなっています。
#対象
railsを触り始めたばかりの初学者向けの内容となっております。もちろん経験者の方大歓迎です。不足している部分や間違っている部分ありましたらご指摘いただけますと大変嬉しいです。
##Transactionの目的
複数のSQL文の変更に対して、全てのアクションが成功した際にDBの変更を発生させるという条件を守らせるために使用するのがTransationの目的です。Transactionにより、データの統一性を保つことができます。
下記のコードは本を100円で購入→本のデータを保存→balance(残高)の保存という3つの処理の例を表しています。この中でどれか一つが失敗する、例えば本を100円で買ったはずがエラーになってしまった。→残高の処理は行われたので所持金は減っているのに本の情報は保存されなかった、などデータの整合性に不具合が生じます。その際にSQL処理を全部ロールバックされるのが、Transactionの特徴です。
この中のどれか一つでも例外が発生するちrollbackしてくれる!
ActiveRecord::Base.transaction do
book.buy(100) # 本を100円で買うメソッド
book.save! # 上記の本の情報が登録される処理
balance.save! # 自分の残高の保存
end
##Transactionはクラスメソッドでもインスタンスでも使える!
Railsでは、トランザクションはすべてのActiveRecordモデルでクラスメソッドとインスタンスメソッドとして利用できます。つまり、以下のアプローチはどちらも同じように有効です。
クラス変数
Client.transaction do
@client.users.create!
@user.clients(true).first.destroy!
Product.first.destroy!
end
インスタンス変数
@client.transaction do
@client.users.create!
@user.clients(true).first.destroy!
Product.first.destroy!
end
上記のコードの中でトランザクションで参照されるモデルクラスがいくつか異なることに気づいたでしょうか?
1つのトランザクション・ブロック内でモデル・タイプが混在していても全く問題ありません。
これは、トランザクションがモデルインスタンスではなくデータベース接続にバインドされているからです。原則として、トランザクションが必要になるのは、複数のレコードへの変更が単一のユニットとして成功しなければならない場合だけです。(ユニットとは、、、本を買う→本の情報保存→自分の残高を減らすという一連の処理のまとまりの事)さらに、Railsはすでに#saveと#destroyメソッドをトランザクションでラップしているので、1つのレコードを更新するときにトランザクションが必要になることはありません。
##Transactionのrollbackが発生する条件
rollbackが発生するには、「例外」が必要となります。
上記コードではsaveに!をつけていますが、これがついているメソッドは、失敗したら例外を吐くメソッドです、transactionを使うときは、saveではなくsave!、destroyではなくdestroy!を使うとtransactionがちゃんと拾ってくれます。これをつけないと不整合なデータが保存validationの網をかいくぐって保存されてしまう可能性もあります。
ActiveRecord::Base.transaction do
david.update_attribute(:amount, david.amount -100)
mary.update_attribute(:amount, 100)
end
例えば上記のコードではupdate_attributeを使用していますがrailsでは#update_attributeは更新に失敗したときに例外を投げないように設計されています。これはfalseを返すので、使用するメソッドが失敗したときに例外を投げるようにする必要があります。先ほどの例の書き方は次のようになります。
ActiveRecord::Base.transaction do
david.update_attributes!(:amount => -100)
mary.update_attributes!(:amount => 100)
end
次の例は#find_byメソッドを使ってトランザクション内でレコードを見つけています。find_byはレコードが返されなかった場合にnilを返すように設計されていますが
通常の #findメソッドは ActiveRecord::RecordNotFound 例外を吐きますよね。
ActiveRecord::Base.transaction do
david = User.find_by_name("david")
if(david.id != john.id)
john.update_attributes!(:amount => -100)
mary.update_attributes!(:amount => 100)
end
end
上記のコードは問題があります。どこかわかりますか?
正解はfind_byメソッドの結果がnilであった場合変数davidにnilが代入され、条件分岐をスルーしてしまうところです。
これでは意図しない結果になってしまうでしょう。下記が適切です。
ActiveRecord::Base.transaction do
david = User.find_by_name("david")
raise ActiveRecord::RecordNotFound if david.nil?
if(david.id != john.id)
john.update_attributes!(:amount => -100)
mary.update_attributes!(:amount => 100)
end
end
find_byに対しての例外が発生すると、ロールバックが完了した後、トランザクションの外で例外が発生します。この例外をキャッチし処理をするコードを準備する必要がありますね。
###例外を発生させずにロールバックさせる方法
例外を使わずに、Transactionをロールバックさせたい場合は、ActiveRecord::Rollbackを使いましょう。
ActiveRecord::Rollback を使用することで、例外を発生させずにトランザクションを実行することも可能です。これにより、コード内の他の場所でレスキューする必要がなく、トランザクションを無効にしてデータベースレコードをリセットすることができます。
###トランザクションを使う上で避けるべきよくあるパターン
単一のレコードのみを更新する
無駄に入れ子にしている
ロールバックが発生しないコードを含むトランザクション
コントローラーでのトランザクションの使用
以上となります。随時新たに知ったこと等あれば追記していこうと思っています。
ここまで見て頂き本当にありがとうございました!