LoginSignup
11
8

More than 3 years have passed since last update.

RailsのTransactionの使い方について

Posted at

はじめに

はじめまして。
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 を使用することで、例外を発生させずにトランザクションを実行することも可能です。これにより、コード内の他の場所でレスキューする必要がなく、トランザクションを無効にしてデータベースレコードをリセットすることができます。

トランザクションを使う上で避けるべきよくあるパターン

単一のレコードのみを更新する
無駄に入れ子にしている
ロールバックが発生しないコードを含むトランザクション
コントローラーでのトランザクションの使用

以上となります。随時新たに知ったこと等あれば追記していこうと思っています。
ここまで見て頂き本当にありがとうございました!

11
8
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
11
8