◆当サイトで人気のプログラミング教室のおすすめランキングはこちら!
プログラミングは独学では非効率で、時間を無駄にするリスクがあります。効率的なカリキュラムで学べるスクールを受講しましょう。

DMM WEBCAMP転職保証付き!エンジニアとして転職したい人におすすめ!
未経験からプロのエンジニアへ3ヶ月で転職する為のスクールです!
TechAcademyオンラインで開講しているプログラミングスクール
オンラインでどこでも学べる!/教室に行くのが忙しい人でも安心!
TECH::CAMP教養としてのITスキルを学べるスクール
Webデザイン/AI(人工知能)/IOS/Androidアプリ制作/VRを学びたい方!
DMM WEBCAMP ビジネス教養コース【マンツーマンサポート】1ヶ月短期集中でプログラミングを学ぶスクール
1ヶ月通い放題・メンター常駐の教室環境でプログラミングを学びたい方!
2月生募集中!当社人気の転職保証コース
プログラミング学習から転職成功まで導く、当社人気のDMM WEBCAMP(旧WEBCAMP PRO)。
1月生は満員となっております。2月生募集に向け、お早めの申込みをオススメします。
プログラミング未経験でもエンジニア転職を絶対成功させたい
スキルを身に着けて人生を自ら切り開きたい
上記にあてはまる方は、ぜひご検討ください!

はじめに

Railsアプリケーションを作る上で、通常の問題の起きない処理(正常系と呼びます)だけを考慮していては、実際の運用に耐えられるものにはなりません。

ユーザーが思いもよらないデータを入力することもありますし、想定外のデータ量があるかもしれません。それに、システムが突然ダウンすることもあるのです。

そういった正常系ではあり得ない状態(異常系と呼びます)も想定して、問題なく処理が進む、もしくは正常に終了するように作り込むのが、プログラムで重要なポイントです。

transactionメソッドは、そんな異常系の中でもデータベースとのやりとり時の異常系に対する処理を作り込むときに使用するメソッドです。

詳しく解説していきましょう。

transactionとは

そもそもトランザクションというのは、データベースへのアクセスにおいて、分割不可能な一連の処理のことを指します。

とてもありがちな例ですが、例えば、AさんからBさんへ送金するとき、以下のように、データベースへのアクセスは2回発生します。

1.Aさんの口座から出金(残高が減る)
2.Bさんの口座へ入金される(残高が増える)

この2つのデータベースへのアクセスは、どちらかの処理が失敗した場合、成り立ちません。

1.だけ成功して2.に失敗しては、お金が失われるだけですし、その逆の場合は、何もしなくてもお金が増えてしまいます。

そのため、データベースの世界では、この2つのアクセスをトランザクションというひとまとまりで扱い、どちらかが失敗した場合は、「処理失敗」としてすべて元に戻す(ロールバックと呼びます)ということを行います。

そんなデータベースの世界では当たり前とも言える処理をRails上で実現するのが、transactionメソッドなのです。

トランザクションはできる限り小さく

トランザクションの中身を見てみると、それぞれのデータベースへのアクセスは、独立して行うことができるものです。しかし、前述のように、データの整合性を保つためには、分けることができないのです。

そのため、トランザクションは不可分だと言われます。つまり、それ以上分けることができない処理単位ということです。

しかし、本当に分けることができないのかは、きちんと確認しておかなければいけません。

トランザクションは、不可分の処理であるため、その処理中は、対象のデータベースの該当箇所についてはロックされ、変更できなくなります。

例えば、Aさんの口座の処理中で、Bさんの口座の処理が行われていなくても、Bさんの口座にはアクセスできなくなるわけです。これは、Aさんの口座からの出金処理が終わる前に、Bさんの口座が解約されてしまうようなことがないようにするため、仕方ありません。

しかし、トランザクションには、3つ以上のデータベースへのアクセスを含むこともできます。そうなると、いくつものデータベースが処理待ちのままアクセス禁止の状態になってしまうということが起こります。複数のデータベースアクセスであれば、処理時間もそれだけ長くなるでしょう。

そうなると、ユーザーの利便性は著しく低下することになってしまうのです。

安易にトランザクションを作ると、いくつものデータベースへのアクセスが含まれてしまうこともあります。そんなときは、改めて、それらは不可分の処理なのかを検討し、トランザクションはできる限り小さい単位にするようにしましょう。

コツコツ独学×スクールで実践。未経験からエンジニアに転職!【WebCamp卒業生インタビュー】

transactionメソッドの使い方

前述までで、トランザクションについて説明しました。単純にまとめると、以下の3点に絞られるでしょう。

・複数のデータベースへのアクセスを1つのまとまった処理として扱う
・1つでも処理に失敗した場合は、トランザクション全体として失敗となる
・失敗した場合は、すべてのデータベースの状態が元に戻る

これらのことを実現するtransactionメソッドについて、説明していきましょう。

基本的な構文

まずは、transactionメソッドの基本的な構文を紹介します。

モデル.transaction do
  テーブルへのアクセス処理
  テーブルへのアクセス処理
    :
end
  トランザクションの処理が成功した場合の処理
rescue => e
  トランザクションの処理が失敗した場合の処理  

モデルについて

transactionメソッドが生成するトランザクションは、1つのデータベースに紐づいて、そのデータベース上のモデル(テーブル)へのアクセスをまとめるものです。

そのため、構文の「モデル」については、ブロック内に含まれるモデルどれを使用しても同じになります。

例えば、itemモデルとuserモデルがあった場合、以下はすべて同じトランザクションになります。

@item.transaction do
  @item.アクセス処理
  @user.アクセス処理
end
@user.transaction do
  @item.アクセス処理
  @user.アクセス処理
end
ActiveRecord::Base.transaction do
  @item.アクセス処理
  @user.アクセス処理
end

最後の「ActiveRecord::Base」は、モデルの親クラスですので、すべてのトランザクションで利用可能です。

ただし、Railsのトランザクションは、あくまでも1つのデータベースと紐づいていますので、別々のデータベースのモデル(テーブル)へのアクセスについては、1つのトランザクションにまとめられませんので、注意が必要です。
(トランザクションの中にトランザクションを含めることでまとめることができますが、処理が複雑になりますので、できる限り避けるべきでしょう)

テーブルへのアクセス処理について

transactionメソッドのブロックの中に記載するテーブルへのアクセス処理は、前述のように同一データベース上のモデルへのアクセス処理でなければいけません。

ただし、transactionメソッドのブロックの中に記載するテーブルへのアクセス処理については、もう1つ注意点がありますので、お伝えしておきます。

それは、「失敗時に例外を発生させるメソッドを使用する」ことです。

すでに説明したとおり、トランザクションは、含まれる処理のいずれか1つでも失敗すると、トランザクション内のすべての処理をロールバックします。しかし、この「ロールバック」を行う条件が、例外の発生なのです。

モデルへアクセスするメソッドには、処理に成功したらtrue、失敗したらfalseを返すものが多く含まれます。これらのメソッドでは処理失敗時の手続きをプログラマが記載することを前提に作られています。

transactionメソッドは、そういった「想定内の失敗」ではなく、「想定外の失敗」に対する処理とも言えますので、例外にのみ反応してロールバックを行うのです。

そのため、処理失敗時に例外を発生させるメソッドを使わなければ、動きません。注意してください。

なお、当然ですが、モデルへのアクセス処理そのもの以外でも、必要であればロールバックさせてもかまいません。取得したデータの加工や検索などで失敗した場合に、その処理を行うメソッドが例外を発生すれば、モデルはロールバックされます。

transactionメソッドの処理結果による分岐

構文には、transactionメソッドの処理結果によって処理が分岐させることができるようになっています。

前述のように、transactionメソッドがロールバックを行うのは、例外発生時です。

じつは、このロールバックを起こした例外については、transactionメソッドがロールバックを行った後、そのままスルーされるため、どこかで受け取って処理しなければ、アプリケーションが停止してしまうのです。
※例外やrescueなどについては、「【Ruby入門説明書】rescueについて解説」を参照してください

そのため、「rescue => e」の後の処理を作っておかなければいけません。

なにより、トランザクションが失敗するというのは、重大なエラーであることが多いため、後処理などを行って正常に終了するなどの処理を記載しておくべきでしょう。

なお、この例外処理を省略する方法として、ActiveRecord::Rollbackを発生させる方法があります。

前述の例外が発生しないメソッドを使い、そのメソッドが失敗した(falseが返ってきた)場合に、以下のようにしてActiveRecord::Rollbackを発生させます。

raise ActiveRecord::Rollback

ActiveRecord::Rollbackでロールバックした場合は、例外が発生しませんので、「rescue => e」の後の処理は不要になるでしょう。

ただし、ロールバックそのものが失敗する可能性もゼロではありません。その場合は例外発生を回避することができませんので、「rescue => e」の後の処理は必須と考えたほうが無難かもしれません。

未経験から上京し、エンジニアとしてチームラボグループに転職!【WebCampPro卒業生インタビュー】

transactionメソッドを使ってみる

ここまでで説明したtransactionメソッドの説明を踏まえて、実際に動かして動作を確認してみましょう。

環境構築

テスト環境は、コマンドプロンプトで以下のコマンドを実行してください。

rails new tx_test
cd tx_test
rails generate scaffold Store item:string stock:integer
rails generate scaffold Farmer item:string stock:integer
rake db:migrate
rails c
Store.create(item:"carrot",stock:0)
Store.create(item:"radish",stock:10)
Store.create(item:"potato",stock:50)
Farmer.create(item:"carrot",stock:100)
Farmer.create(item:"radish",stock:10)
Farmer.create(item:"potato",stock:30)
exit

一度、動作確認しておきましょう。

「rails s」でサーバーを起動して、「localhost:3000/stores」と「localhost:3000/farmers」にアクセスすると、以下のように表示されます。
(localhost:3000/stores)

(localhost:3000/farmers)

なお、この先の確認を簡単にするため、これらは別のウインドウで表示しておくことをおすすめします。

トランザクションの実装

FarmerモデルからStoreモデルへ商品を出荷する処理を実装します。

この処理は、前述した送入金と同じく、Farmerモデルの在庫を減らす(出荷)と同時に、Storeモデルの在庫を増やす(入荷)必要がありますので、出荷と入荷で1つの処理にしなければ都合が悪いでしょう。

そこで、Storeモデルの一覧画面の「Edit」を入荷処理に変更します。「Edit」をクリックすることで、1つ入荷処理を行うようにしましょう。なお、同時にFarmerモデルから同じ商品を1つ減らします。

Storeコントローラのeditメソッドを、次のように変更します。(そもそも空のメソッドですので、追加することになります)
(app/controllers/stores_controller.rb)

    :
  def edit
    farmer = Farmer.find_by!(item: @store.item)  # 同じ商品のFarmerモデルを取得

    # トランザクション
    @store.transaction do
      farmer.decrement!(:stock, 1)  # Farmerモデルから1つ減らす
      @store.increment!(:stock, 1)  # Storeモデルを1つ増やす
    end

      redirect_to "/stores"         # 一覧画面へ遷移(編集画面を表示しない)
    rescue => e
      puts e.message                # エラーメッセージをログ出力して
      redirect_to "/stores"         # 一覧画面へ遷移(編集画面を表示しない)
  end
    :

なお、decrement!メソッドは、整数型のカラムから第2引数の数を引いて、テーブルを更新するメソッドです。increment!メソッドは、同様に整数型のカラムに第2引数の数を足して、テーブルを更新します。
※両方とも、末尾に「!」がついている破壊的メソッドで、テーブル更新失敗時は例外が発生するメソッドです。同じ処理を行うメソッドで「!」のついていないものもありますので、気を付けてください。

つまり、この2行がテーブルへアクセスしている処理ですので、transactionメソッドのブロックへ入れています。

その後、トランザクションが成功すれば一覧画面へリダイレクトさせています。トランザクション失敗時も、エラーメッセージを出力してから、一覧画面を表示させています。

どちらも一覧画面へ遷移させているのは、stockの変化を確認するためです。

実際に動作確認してみましょう。

Storeモデルの一覧画面で、carrotの行の「Edit」をクリックすると、carrotのstockが1つ増えます。

同時に、Farmerモデルのcarrotが1つ減っていることも確認できます。(F5キーなどで更新する必要があります)

トランザクションの処理失敗時の動き

では、トランザクション内で処理が失敗した場合の動きを確認してみましょう。

失敗させるために、先ほど実装したトランザクション内を修正します。
(app/controllers/stores_controller.rb)

    :
    # トランザクション
    @store.transaction do
      farmer.decrement!(:stock, 1)  # Farmerモデルから1つ減らす
      @store.increment!(:stock, "1")  # Storeモデルを1つ増やす
    end
    :

強引ですが、Storeモデルのincrement!メソッドの第2引数を文字列にします。

この状態で、Storeモデルの一覧画面の「Edit」をクリックしてみましょう。

画面がちらつくかもしれませんが、stockに変化がないと思います。Farmerモデルのstockも変わっていません。

これはエラーが発生して、処理がなかったことになっているからです。

詳細な動きは、コマンドプロンプトに表示されているログから確認できますので、抜粋したものを紹介しましょう。(ログ)

    :
   (0.0ms)  begin transaction
  Farmer Update All (0.0ms)  UPDATE "farmers" SET "stock" = COALESCE("stock", 0) - 1 WHERE "farmers"."id" = ?  [["id", 1]]
   (33.0ms)  rollback transaction
String can't be coerced into Integer
Redirected to http://localhost:3000/stores
    :

「begin transaction」でトランザクション処理が開始され、まずFarmerモデルに対して、1つ減らすSQLが処理されています。

しかし、その後に行われるはずのStoreモデルに対するSQLが処理されていません。これは、第2引数を文字列にしたため、例外が発生したためです。

その代わりに「rollback transaction」というメッセージが表示されています。ここで、トランザクションが失敗したために、1度更新したFarmerモデルを元に戻しているわけです。

その次の「String can't be coerced into Integer」は、「rescue => e」の後に追記した、例外の内容を表すメッセージ表示です

以上が、トランザクションの動きです。

このように、テーブルへ登録したものがあるにもかかわらず、トランザクションが完了しなかった場合は、ただちにロールバックが行われますので、途中までの処理が残ってしまうことなく、データの整合性が保たれるのです。

“未経験”でもたった1ヶ月で営業からエンジニアとして転職!『WebCamp』受講者インタビュー

まとめ

今回は、トランザクションの説明と、Railsでの実装方法について、説明しました。

トランザクションは、データベースを扱う上でとても大事な概念ですので、しっかりとイメージできるようになっておいてください。

トランザクションは、データの整合性を守る強力な武器ですが、使いすぎると使いやすさを犠牲にしてしまいます。そのため、最低限必要な部分に利用することを意識しなければいけません。

しかし、そのバランスがうまくとれれば、データの整合性が崩れない安全性と、使いやすさを両立したシステムを作ることも可能なのです。

ただし、こういった直感や経験が必要な部分が、やはり現場を経験した技術者に直接話を聞くのが効果的でしょう。ぜひ、スクールなども活用して、どういったシーンでどの程度トランザクションを使うべきなのか、理解を深めていただければと思います。

・transactionは、複数のテーブルへのアクセスを1つの処理として扱って、データの整合性を保つためのもの
・複数のアクセスのうち1つでも失敗すると、すべてのアクセスがなかったことになる(ロールバックされる)
・例外が発生することで、transactionが失敗と認識する
・ロールバック後、transaction内で発生した例外が改めた発生する(ActiveRecord::Rollback以外)
・transactionは、1つのデータベースに対して紐づけられているため、複数のモデル(テーブル)への処理をまとめられる
・複数のデータベースに対するtransactionは、transactionをネストさせることで実現可能

DMM WEWBCAMPについて

DMM WEBCAMPは3ヶ月間で未経験から即戦力エンジニアを育成する転職保障付きのプログラミングスクールです。1ヶ月でプログラミング・Webデザインを学ぶ通い放題の「ビジネス教養コース」も展開しています。

DMM WEBCAMPを運営する株式会社インフラトップ では、「学びと仕事を通して人生を最高の物語にする」という理念で会社を経営しています。

キャリアアップを目指す方は、この機会に私達と一緒にプログラミングを学んでみませんか?

2月枠も残りわずか当社人気の転職保証コース
プログラミング学習から転職成功まで導く、当社人気のDMM WEBCAMP(旧WEBCAMP PRO)。
1月受入枠は満員となっております。2月枠に向け、お早めの申込みをオススメします。
プログラミング未経験でもエンジニア転職を絶対成功させたい
スキルを身に着けて人生を自ら切り開きたい
上記にあてはまる方は、ぜひご検討ください!

▼未経験から1ヶ月でWEBデザイン・プログラミングを学びたい方はこちら!


▼ついに開講!オンラインでWEBデザイン・プログラミングを学びたい方はこちら!

 

【インタビュー】1ヶ月でRubyをゼロから学び、Webエンジニアとして転職!

ブラジルから帰国し技術をつけようとRubyエンジニアを目指してWebCampでRubyを学び、見事Webエンジニアとして転職を果たした田中さんにお話を伺いました。

Rubyの学習がしたい。基礎をしっかりと理解したい

転職のサポートがほしい

と考えている方はぜひお読み下さい。

【WebCamp卒業生インタビュー】1ヶ月でRubyをゼロから学び、Webエンジニアとして転職!

おすすめの記事