質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

91.24%

  • Ruby

    5469questions

    Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

  • Ruby on Rails

    5366questions

    Ruby on Railsは、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

railsで複数のモデルにまたがるトランザクション処理で中途半端にモデルが更新されてしまう

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 130

zendendo

score 15

概要

ポイント(数量)を取引振替する機能をつくっていて、
ユーザーが取引を入力すると、3つのモデルが更新されることになっています。
そこで、
複数のモデルへの同時更新が完全成功しない限り、モデルの更新確定できないようにしたい
もしも、複数のモデルに記述してあるvalidationにどれか一つに引っかかって記録ができないのならば、同時更新できないとみなされて取引が無効になるのでは?
と考え、3つのモデルにまたがるトランザクション処理を記述しました。

モデルA.transaction do
  #モデルAでやりたい処理
  モデルB.transaction do
    # モデルBでやりたい処理
    モデルC.transaction do
      # モデルCでやりたい処理
    end
  end
end


正しくない値(3つのモデルの内、1つのモデルのバリデーションに引っかかる値)を入力して取引が無効になるか試したのですが、その結果・・・
1つのモデルは更新失敗、残り2つのモデルは更新成功するという不完全な状態で処理が確定されてしまいました。
イメージ説明
どうすれば、
「同時更新に完全成功したときのみモデルへの処理を確定」
「不完全な更新なら、途中成功したモデル更新を含め全てなかったことにする」
という処理にできるのか教えて頂ければ幸いです。

具体的な状況説明

ユーザーが取引を入力すると、
AccountTransactionモデル(誰と誰がいくら取引したいか記録)
DepositsAndWithdrawalモデル(誰がいくら支払い受け取ったか記録)
BasicIncomeAcountモデル(互いの残高が更新される)
計3つのモデルが動くことになっています。

以下の図は正しい値が入力された場合
イメージ説明

以下のコントローラファイルに、3つのモデルを更新するためのトランザクション処理を記述しています。

class AccountTransactionsController < ApplicationController

  #取引を作成する 
  def new
    @account_transaction = AccountTransaction.new
  end

  #3つのモデルを更新する処理
  def create
  #一つ目のモデルAccountTransactionを更新
    AccountTransaction.transaction do
      @account_transaction = AccountTransaction.new(        
        withdrawal_account_id: current_user.basic_income_account.id,
        deposit_account_id: BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]).id,
        amount: params[:account_transaction][:amount]
        )
      @account_transaction.save!
      #2つ目のモデルDepositsAndWithdrawalを更新
      DepositsAndWithdrawal.transaction do
        #出金側レコードを作成保存
        @account_transaction.deposits_and_withdrawals.build(
          transaction_type: "出金",
          basic_income_account_id: current_user.basic_income_account.id,
          amount: -1 * params[:account_transaction][:amount].to_f
          )
        @account_transaction.save!
        #出金側レコードを作成保存
        @account_transaction.deposits_and_withdrawals.build(
          transaction_type: "入金",
          basic_income_account_id: BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]).id,
          amount: params[:account_transaction][:amount].to_f
          )
        @account_transaction.save!
        #3つ目のモデルBasicIncomeAccount(口座)を更新
        BasicIncomeAccount.transaction do
          @withdrawal_basic_income_account = current_user.basic_income_account#出金口座
          @deposit_basic_income_account = BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id])#入金口座       
          #取引額を代入する
          @amount = params[:account_transaction][:amount].to_f
          #出金口座からは取引額を引き、入金口座には取引額を足して更新する
          @withdrawal_basic_income_account.update(balance: @withdrawal_basic_income_account.balance - @amount)
          @deposit_basic_income_account.update(balance: @deposit_basic_income_account.balance + @amount)
        end
      end
    end
    redirect_to root_path#トランザクション成功とみなし、root_path(ホーム画面)に戻る
  rescue => e
    render plain: e.message

  end

  private
  def account_transaction_params
  params.require(:account_transaction).permit(:deposit_account_id, :amount)
  end
end

ログインしたユーザーが保有する口座(BasicIncomeAcountモデル)残高が100だとします。
残高以上の支払いをして口座がマイナス数値になるのは困るので、
残高が記録されているモデルのmodelファイルであるbasic_income_account.rbには、
validates :balance, presence: true, numericality: {greater_than_or_equal_to: 0.00}
という記述をしました。(残高が0以上なら検証成功としモデルを更新する)
そこで残高を超える取引額101を入力すると、BasicIncomeAcountモデルは更新されませんでしたが、
AccountTransactionモデルとDepositsAndWithdrawalモデルは更新されてしまっています。

それぞれのmodelファイルの記述です。

class AccountTransaction < ApplicationRecord

  belongs_to :withdrawal, class_name: 'BasicIncomeAccount', :foreign_key => 'withdrawal_account_id'
  belongs_to :deposit, class_name: 'BasicIncomeAccount', :foreign_key => 'deposit_account_id'

  #支払元の口座番号が存在しているならば検証成功
  validates :withdrawal_account_id, presence:true
  #取引相手(支払先)の口座番号が存在しているなら検証成功
  validates :deposit_account_id, presence:true
  #amount(取引額)が入力され、入力された内容が数値か小数点なら検証成功,
  validates :amount, presence: true, numericality: true

  has_many :deposits_and_withdrawals

end
class DepositsAndWithdrawal < ApplicationRecord
    belongs_to :account_transaction

    #account_transaction_id(取引ID)が存在しているならば検証成功
    validates :account_transaction_id, presence:true
    #amount(取引額)は、数値か小数点なら検証成功
    validates :amount, presence: true, numericality: true

    belongs_to :basic_income_account
end
class BasicIncomeAccount < ApplicationRecord
    belongs_to :user

    #ユーザーIDが存在するなら検証成功
    validates :user_id, presence:true
    #口座番号が存在し、値(口座番号)がユニーク(被らない番号)なら検証成功
    validates :account_number, presence: true, uniqueness: true
    #口座残高は数値か小数点であり、計算の結果、指定値0.00以上になる(残高がマイナスにならない)なら検証成功
    validates :balance, presence: true, numericality: {greater_than_or_equal_to: 0.00}

    has_many :withdrawal_account_transaction, class_name: 'AccountTransaction', :foreign_key => 'withdrawal_account_id'
    has_many :deposit_account_transaction, class_name: 'AccountTransaction', :foreign_key => 'deposit_account_id'
end

それぞれのテーブルの内容です。

ActiveRecord::Schema.define(version: 20171125094439) do

  create_table "account_transactions", force: :cascade do |t|
    t.decimal "amount"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "withdrawal_account_id"
    t.integer "deposit_account_id"
    t.index ["deposit_account_id"], name: "index_account_transactions_on_deposit_account_id"
    t.index ["withdrawal_account_id"], name: "index_account_transactions_on_withdrawal_account_id"
  end

  create_table "basic_income_accounts", force: :cascade do |t|
    t.integer "user_id"
    t.string "account_number"
    t.decimal "balance"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_basic_income_accounts_on_user_id"
  end

  create_table "deposits_and_withdrawals", force: :cascade do |t|
    t.integer "account_transaction_id"
    t.string "transaction_type"
    t.decimal "amount"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "basic_income_account_id"
    t.index ["account_transaction_id"], name: "index_deposits_and_withdrawals_on_account_transaction_id"
    t.index ["basic_income_account_id"], name: "index_deposits_and_withdrawals_on_basic_income_account_id"
  end

追記(最終的に成功したコード)
自己解決ではないので、ここに最終的に成功したコードを記述します。

class AccountTransactionsController < ApplicationController

  def new
    @account_transaction = AccountTransaction.new
  end

  def create
    begin
      ActiveRecord::Base.transaction{
        @account_transaction = AccountTransaction.new(
          withdrawal_account_id: current_user.basic_income_account.id,
          deposit_account_id: BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]).id,
          amount: params[:account_transaction][:amount]
          )
        @account_transaction.save!

        @account_transaction.deposits_and_withdrawals.build(
          transaction_type: "出金",
          basic_income_account_id: current_user.basic_income_account.id,
          amount: -1 * params[:account_transaction][:amount].to_f
        )
        @account_transaction.deposits_and_withdrawals.build(
          transaction_type: "入金",
          basic_income_account_id: BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]).id,
          amount: params[:account_transaction][:amount].to_f
          )
        @account_transaction.save!


        @withdrawal_basic_income_account = current_user.basic_income_account
        @deposit_basic_income_account = BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id])

        @amount = params[:account_transaction][:amount].to_f

        @withdrawal_basic_income_account.update!(balance: @withdrawal_basic_income_account.balance - @amount)
        @deposit_basic_income_account.update!(balance: @deposit_basic_income_account.balance + @amount)

      }
      #完全成功なら、root_path(ホーム画面へ)
      redirect_to root_path
    #以下は例外が発生(プロセスの内どこかが失敗)したときに行う。rescueは自分でカスタマイズした例外処理を行えるが、railsが自動的に表示してくれるエラーを見たければ一旦resucueをコメントアウトしてみよう。
    rescue e.message
    #flash[:notice] = "失敗しました。リトライしてみてください"
    #render "new"         
      #rollback
    end
  end

補足情報(言語/FW/ツール等のバージョンなど)

Rails 5.1.3
ruby 2.4.1

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

私はトランザクションの入れ子は制御が難しいので、一度も使ったことがありません。
特に今回の話だと、3つのモデル=テーブルの更新を1つの塊として処理したいという事なので、入れ子にする理由もないですよね。

自分はいつもこんな感じでやっています。

begin
  ActiveRecord::Base.transaction {
     モデルA.save!

     # 途中でなんらかの条件によりエラーと判定したい場合
     # rollbackしたいので、例外を自分で発生させる
     if xxxx
       raise "[error] rollback!"
     end

     モデルB.save!
     モデルC.save!
  }
rescue Exception => e
  # rollback
end

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/12/19 16:17 編集

    mingosさん、回答して下さいありがとうございます。
    アドバイスに従い・・・トランザクション処理を入れ子(ネスト)ではない書き方をすることで、解決することができました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

ただいまの回答率

91.24%

関連した質問

同じタグがついた質問を見る

  • Ruby

    5469questions

    Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

  • Ruby on Rails

    5366questions

    Ruby on Railsは、オープンソースのWebアプリケーションフレームワークです。「同じことを繰り返さない」というRailsの基本理念のもと、他のフレームワークより少ないコードで簡単に開発できるよう設計されています。

  • トップ
  • Rubyに関する質問
  • railsで複数のモデルにまたがるトランザクション処理で中途半端にモデルが更新されてしまう