【Rails入門】saveがすべて成功したことを保証する(transaction)

複数の処理をまとめて処理 transactionを利用しよう

みなさんは、トランザクション(transactioni)を知っていますか?

データベース界隈でよく聞くトランザクションですが、データベースを扱うRuby on Rails(以降、Rails)でも当然のようにトランザクションを利用できます。

この記事では、トランザクションを利用する方法を説明しながら、以下のような疑問に答えます。


トランザクションって聞いたことはあるけど
もう一度説明して欲しい

Railsでもトランザクションを利用できるの?
transactionメソッドの使用例をみたい!
saveメソッドとsave!メソッドの使い分けを知りたい


それでは、始めましょう。

トランザクションとは

トランザクションとは、複数の処理をまとめて大きな1つの処理として扱うための機能です。

よく言われる例ですが、まりこさんがさとるくんに、銀行のATMで6000円送金する場合を考えてみましょう。送金の手順を単純化すると、2つの処理と考えられます。

(1)まりこさんがATMに6000円を入れる
(2)さとるくんの口座に6000円が入る

ただし、この2つの処理は、送金という大きな1つの処理と考えるべきでしょう。なぜなら、2つの処理を分割して考えてしまうと、まりこさんがATMに6000円を入れた後にシステムトラブルが発生したら、以下のようになってしまうためです。

(1)まりこさんはATMに6000円入れた
(2)さとるくんの口座にお金が入らない

このケースでの正しい対処方法は、2つ考えられます。

(A)まりこさんに6000円返金する
(B)(システムトラブルが解決した後)さとるくんの口座に6000円が入る

トランザクションは、(A)の方法で対処する場合に使います。

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 両対応)
更新日 : 2019年8月9日

Linux Mintのインストールについては、以下の記事で詳しく説明しています。

Linux Mint Cinnamonエディションを使ってみよう
更新日 : 2019年5月22日

(2)端末で以下のコマンドを1行ずつ順番に入力します

priceは整数でなくてはならないという制約をつけましょう。制約を守らなかった場合に例外が発生しますので、トランザクションのテスト用にはうってつけです。

(3)app/models/item.rbを編集します。

変更前:

変更後:

transactionメソッドの基本構文

transactionメソッドの基本的な書きかたは以下のとおりです。とてもシンプルな構造になっていますので、コメント以外の説明は必要ないでしょう。

なお、トランザクションはデータベースにデータを保存する処理を正しく管理することが目的のため、モデル名とは別のモデルをtransactionメソッド内で使用しても問題ありません。

save!メソッドでがすべて成功した場合

Railsでのトランザクションは、複数のsave!メソッド(またはsaveメソッド)を実行するときに使います。

まずは、2つのsave!メソッドが無事に終了する例を紹介しましょう。

(1)app/controllers/trans_controller.rbを編集します。

変更前:

変更後:

(2)ブラウザで「http://localhost:3000/trans/example」にアクセスします。

以下のように表示されます。

(3)ブラウザで「http://localhost:3000/items」にアクセスします。

以下のように、腕時計とオルガンのデータが登録されたことが確認できます。

save!メソッドの一部でバリデーションエラーが発生した場合

次は、バリデーションエラーが発生した場合の動作を確認しましょう。

(1)app/controllers/trans_controller.rbを編集します。

変更前:

変更後:

まくらの金額に小数(2400.5)を設定しました。app/models/item.rbで、整数のみという制約を付けましたので、これはバリデーションエラーが発生するはずです。

(2)ブラウザで「http://localhost:3000/trans/example」にアクセスします。

以下のように表示され、バリデーションエラーが発生していることがわかります。

鏡とまくらのデータは登録されているでしょうか。

(3)ブラウザで「http://localhost:3000/items」にアクセスします。

以下のように、腕時計とオルガンのデータだけが登録されたままで、まくらはもちろん、鏡のデータも登録されていないことが確認できます。

ログも確認しておきましょう。

(4)新しい「端末」を起動して、以下のコマンドを1行ずつ順番に入力します。

実行結果:

「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を編集します。

変更前:

変更後:

まくらの金額に小数(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行ずつ順番に入力します。

実行結果:

SQLを発行する時点で、まくらのデータが「2400.5」から「2400」に変更されていることがわかります。

分離レベルを知ろう

分離レベルに対応するデータベースを使用している場合は、トランザクションで分離レベルを指定できます。

分離レベルは、複数のトランザクションを同時に実行した場合にどの程度データに一貫性を持たせるのかを指定するためのものです。分離レベルが高いほど、データの一貫性を保ちやすくなりますが、代わりに複数のユーザーがデータに同時にアクセスできなくなります。

少し具体例を説明しましょう。先ほどのデータのロールバック(巻き戻し)が発生した例を思い出してください。

説明のために、ログを再掲します。

「INSERT INTO」から「rollback transaction」が表示されるまでのほんの一瞬(3.1ms)ですが、データベース(テーブル)に鏡のデータが追加されていますね。

この一瞬に、他のユーザーが鏡のデータを読み出してしまうと、本来存在してはいけないデータにアクセスできていることになり、何かしらの問題が発生するでしょう。

このような問題を回避するために、分離レベルを適切に設定します。Railsで用意されている分離レベルは、レベルの低い順に以下の4つです。

:read_uncommitted(最も分離レベルが低い)
:read_committed
:repeatable_read
:serialization(最も分離レベルが高い)

先ほど紹介した問題は、:read_committed、:repeatable_read、:serializationのいずれかを設定すると、回避できます。

分離レベルは以下のように設定します。

変更前:

変更後:

まとめ

この記事では、トランザクション処理を説明しました。

トランザクションは複数の処理をまとめて大きな1つの処理として扱うための機能です。そして、処理の1つで例外(問題)が発生したら、複数の処理をまとめてロールバック(巻き戻す)ことができます。

予期せぬバグを未然に防げるので、使えそうな場面では一連の処理にトランザクションの導入を検討するとよいでしょう。

分離レベルにも気をつけてくださいね。

LINEで送る
Pocket

最短でエンジニアを目指すなら侍エンジニア塾

cta_under_bnr

侍エンジニア塾は業界で初めてマンツーマンレッスンを始めたプログラミングスクールです。これまでの指導実績は16,000名を超え、未経験から数多くのエンジニアを輩出しています。

あなたの目的に合わせてカリキュラムを作成し、現役エンジニア講師が専属であなたの学習をサポートするため効率よく学習を進めることができますよ。

無理な勧誘などは一切ありません。まずは無料体験レッスンを受講ください。

無料体験レッスンの詳細はこちら

書いた人

侍テック編集部

侍テック編集部

おすすめコンテンツ

あなたにぴったりなプログラミング学習プランを無料で診断!

プログラミング学習の効率を劇的に上げる学習メソッドを解説