transactionメソッドを使ってみよう
動作を理解するためにWebアプリを作成する
Railsでトランザクションを使うには、transactionメソッドを使います。transactionメソッドの使い方を理解するために、RailsをインストールしてWebアプリを作りましょう。
(1)先ずはRailsをインストールします
私は、以下の記事を参考に、VirtualBoxで作成した仮想パソコンにインストールしたLinux Mintに、Railsの開発環境を作成しました。
基本的には記事の手順に従って操作しますが、app/samurai/sample1ディレクトリを作成する代わりに、app/samurai/transaction-demoディレクトリを作成しました。また、Railsを起動して、ブラウザで画面が表示されることを確認したら、いったんRailsを終了してから次に進みます。
初心者でもかんたん!Ruby on Rails の開発環境の構築手順(Mac/Windows 両対応)
更新日 : 2020年3月2日
Linux Mintのインストールについては、以下の記事で詳しく説明しています。
Linux Mint Cinnamonエディションを使ってみよう
更新日 : 2020年6月24日
(2)端末で以下のコマンドを1行ずつ順番に入力します
bin/rails generate scaffold Item name:string price:integer
bin/rails generate controller Trans example
bin/rails db:migrate
bin/rails server
priceは整数でなくてはならないという制約をつけましょう。制約を守らなかった場合に例外が発生しますので、トランザクションのテスト用にはうってつけです。
(3)app/models/item.rbを編集します。
変更前:
class Item < ApplicationRecord
end
変更後:
class Item < ApplicationRecord
validates :price, numericality: { only_integer: true }
end
transactionメソッドの基本構文
transactionメソッドの基本的な書きかたは以下のとおりです。とてもシンプルな構造になっていますので、コメント以外の説明は必要ないでしょう。
モデル名.transaction do
# 例外が発生する可能性のある処理
end
# 正常に動作した場合の処理
rescue => e
# 例外が発生した場合の処理
なお、トランザクションはデータベースにデータを保存する処理を正しく管理することが目的のため、モデル名とは別のモデルをtransactionメソッド内で使用しても問題ありません。
save!メソッドでがすべて成功した場合
Railsでのトランザクションは、複数のsave!メソッド(またはsaveメソッド)を実行するときに使います。
まずは、2つのsave!メソッドが無事に終了する例を紹介しましょう。
(1)app/controllers/trans_controller.rbを編集します。
変更前:
class TransController < ApplicationController
def example
end
end
変更後:
class TransController < ApplicationController def example Item.transaction do item1 = Item.new({name:"腕時計", price:23000}) item1.save! item2 = Item.new({name:"オルガン", price:53000}) item2.save! end render plain:'保存に成功しました。' rescue => e
render plain: e.message
end
end
(2)ブラウザで「http://localhost:3000/trans/example」にアクセスします。
以下のように表示されます。
![]()
(3)ブラウザで「http://localhost:3000/items」にアクセスします。
以下のように、腕時計とオルガンのデータが登録されたことが確認できます。
![]()
save!メソッドの一部でバリデーションエラーが発生した場合
次は、バリデーションエラーが発生した場合の動作を確認しましょう。
(1)app/controllers/trans_controller.rbを編集します。
変更前:
class TransController < ApplicationController def example Item.transaction do item1 = Item.new({name:"腕時計", price:23000}) item1.save! item2 = Item.new({name:"オルガン", price:53000}) item2.save! end render plain:'保存に成功しました。' rescue => e
render plain: e.message
end
end
変更後:
class TransController < ApplicationController def example Item.transaction do item1 = Item.new({name:"鏡", price:5400}) item1.save! item2 = Item.new({name:"まくら", price:2400.5}) item2.save! end render plain:'保存に成功しました。' rescue => e
render plain: e.message
end
end
まくらの金額に小数(2400.5)を設定しました。app/models/item.rbで、整数のみという制約を付けましたので、これはバリデーションエラーが発生するはずです。
(2)ブラウザで「http://localhost:3000/trans/example」にアクセスします。
以下のように表示され、バリデーションエラーが発生していることがわかります。
![]()
鏡とまくらのデータは登録されているでしょうか。
(3)ブラウザで「http://localhost:3000/items」にアクセスします。
以下のように、腕時計とオルガンのデータだけが登録されたままで、まくらはもちろん、鏡のデータも登録されていないことが確認できます。
![]()
ログも確認しておきましょう。
(4)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。
cd app/samurai/transaction-demo/log
tail --lines=20 development.log
実行結果:
Started GET "/trans/example" for 127.0.0.1 at 2018-07-11 11:43:18 +0900
Processing by TransController#example as HTML
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "items" ("name", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "鏡"], ["price", 5400], ["created_at", "2018-07-11 02:43:18.565431"], ["updated_at", "2018-07-11 02:43:18.565431"]]
(3.1ms) rollback transaction
Rendering text template
Rendered text template (0.0ms)
Completed 200 OK in 40ms (Views: 2.5ms | ActiveRecord: 5.1ms)
Started GET "/items" for 127.0.0.1 at 2018-07-11 11:44:28 +0900
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (0.2ms) SELECT "items".* FROM "items"
Rendered items/index.html.erb within layouts/application (1.8ms)
Completed 200 OK in 18ms (Views: 16.9ms | ActiveRecord: 0.2ms)
「SQL (0.4ms) INSERT INTO(省略)」(鏡のデータが追加されたログ)の後に「(3.1ms) rollback transaction」と表示されています。これは、ロールバック(巻き戻し)が実行されたことを表しているのです。
saveメソッドでバリデーションを無視して保存するには
バリデーションエラーが発生するようなデータでも無理矢理保存するには、save(validate: false)を使用します。せっかく設定したバリデーションを無視することになりますので、使用する場合はよくよく検討しましょう。
また、使用しているメソッドが、save!メソッドではなく、saveメソッドであることに注意してください。save!メソッドはバリデーションを行い、バリデーションエラーが発生した場合に、例外を発生させます。
saveメソッドはバリデーションを行いますが、バリデーションエラーが発生した場合に、falseを返します(例外を発生させません)。
transactionメソッドは、例外に反応する仕組みですから、トランザクションではsave!メソッドを使うほうが読みやすいでしょう。
(1)app/controllers/trans_controller.rbを編集します。
変更前:
class TransController < ApplicationController def example Item.transaction do item1 = Item.new({name:"鏡", price:5400}) item1.save! item2 = Item.new({name:"まくら", price:2400.5}) item2.save! end render plain:'保存に成功しました。' rescue => e
render plain: e.message
end
end
変更後:
class TransController < ApplicationController def example Item.transaction do item1 = Item.new({name:"鏡", price:5400}) item1.save(validate: false) item2 = Item.new({name:"まくら", price:2400.5}) item2.save(validate: false) end render plain:'保存に成功しました。' rescue => e
render plain: e.message
end
end
まくらの金額に小数(2400.5)を設定したまま、save!()の代わりにsave(validate: false)を使いました。
このコードを実行すると、先ほどと同様のバリデーションエラーが発生するはずですが、save(validate: false)に変更しているため、バリデーションエラーが発生せずにデータが登録されます。
(2)ブラウザで「http://localhost:3000/trans/example」にアクセスします。
以下のように表示されます。
![]()
小数のはずですが、保存に成功したようです。
(3)ブラウザで「http://localhost:3000/items」にアクセスします。
以下のように、今度は鏡もまくらも登録されていることが確認できます。「2400.5」は「2400」で登録されていますね。
![]()
ログも確認しておきましょう。
(4)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。
cd app/samurai/transaction-demo/log
cat development.log
実行結果:
Started GET "/trans/example" for 127.0.0.1 at 2018-07-11 11:55:34 +0900
Processing by TransController#example as HTML
(0.0ms) begin transaction
SQL (0.6ms) INSERT INTO "items" ("name", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "鏡"], ["price", 5400], ["created_at", "2018-07-11 02:55:34.107924"], ["updated_at", "2018-07-11 02:55:34.107924"]]
SQL (0.1ms) INSERT INTO "items" ("name", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "まくら"], ["price", 2400], ["created_at", "2018-07-11 02:55:34.110136"], ["updated_at", "2018-07-11 02:55:34.110136"]]
(15.5ms) commit transaction
Rendering text template
Rendered text template (0.0ms)
Completed 200 OK in 31ms (Views: 1.5ms | ActiveRecord: 18.9ms)
Started GET "/items" for 127.0.0.1 at 2018-07-11 11:55:56 +0900
Processing by ItemsController#index as HTML
Rendering items/index.html.erb within layouts/application
Item Load (0.2ms) SELECT "items".* FROM "items"
Rendered items/index.html.erb within layouts/application (7.5ms)
Completed 200 OK in 36ms (Views: 35.3ms | ActiveRecord: 0.2ms)
SQLを発行する時点で、まくらのデータが「2400.5」から「2400」に変更されていることがわかります。
分離レベルを知ろう
分離レベルに対応するデータベースを使用している場合は、トランザクションで分離レベルを指定できます。
分離レベルは、複数のトランザクションを同時に実行した場合にどの程度データに一貫性を持たせるのかを指定するためのものです。分離レベルが高いほど、データの一貫性を保ちやすくなりますが、代わりに複数のユーザーがデータに同時にアクセスできなくなります。
少し具体例を説明しましょう。先ほどのデータのロールバック(巻き戻し)が発生した例を思い出してください。
説明のために、ログを再掲します。
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "items" ("name", "price", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "鏡"], ["price", 5400], ["created_at", "2018-07-11 02:43:18.565431"], ["updated_at", "2018-07-11 02:43:18.565431"]]
(3.1ms) rollback transaction
「INSERT INTO」から「rollback transaction」が表示されるまでのほんの一瞬(3.1ms)ですが、データベース(テーブル)に鏡のデータが追加されていますね。
この一瞬に、他のユーザーが鏡のデータを読み出してしまうと、本来存在してはいけないデータにアクセスできていることになり、何かしらの問題が発生するでしょう。
このような問題を回避するために、分離レベルを適切に設定します。Railsで用意されている分離レベルは、レベルの低い順に以下の4つです。
:read_uncommitted(最も分離レベルが低い)
:read_committed
:repeatable_read
:serialization(最も分離レベルが高い)
先ほど紹介した問題は、:read_committed、:repeatable_read、:serializationのいずれかを設定すると、回避できます。
分離レベルは以下のように設定します。
変更前:
Item.transaction() do
変更後:
Item.transaction(isolation: :read_committed) do
まとめ
この記事では、トランザクション処理を説明しました。
トランザクションは複数の処理をまとめて大きな1つの処理として扱うための機能です。そして、処理の1つで例外(問題)が発生したら、複数の処理をまとめてロールバック(巻き戻す)ことができます。
予期せぬバグを未然に防げるので、使えそうな場面では一連の処理にトランザクションの導入を検討するとよいでしょう。
分離レベルにも気をつけてくださいね。