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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Ruby

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

Ruby on Rails

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

Q&A

解決済

1回答

2420閲覧

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

zendendo

総合スコア43

Ruby

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

Ruby on Rails

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

0グッド

1クリップ

投稿2017/12/17 02:02

編集2017/12/18 09:29

###概要
ポイント(数量)を取引振替する機能をつくっていて、
ユーザーが取引を入力すると、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

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

ruby

1class AccountTransactionsController < ApplicationController 2 3 def new 4 @account_transaction = AccountTransaction.new 5 end 6 7 def create 8 begin 9 ActiveRecord::Base.transaction{ 10 @account_transaction = AccountTransaction.new( 11 withdrawal_account_id: current_user.basic_income_account.id, 12 deposit_account_id: BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]).id, 13 amount: params[:account_transaction][:amount] 14 ) 15 @account_transaction.save! 16 17 @account_transaction.deposits_and_withdrawals.build( 18 transaction_type: "出金", 19 basic_income_account_id: current_user.basic_income_account.id, 20 amount: -1 * params[:account_transaction][:amount].to_f 21 ) 22 @account_transaction.deposits_and_withdrawals.build( 23 transaction_type: "入金", 24 basic_income_account_id: BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]).id, 25 amount: params[:account_transaction][:amount].to_f 26 ) 27 @account_transaction.save! 28 29 30 @withdrawal_basic_income_account = current_user.basic_income_account 31 @deposit_basic_income_account = BasicIncomeAccount.find_by(account_number: params[:account_transaction][:deposit_account_id]) 32 33 @amount = params[:account_transaction][:amount].to_f 34 35 @withdrawal_basic_income_account.update!(balance: @withdrawal_basic_income_account.balance - @amount) 36 @deposit_basic_income_account.update!(balance: @deposit_basic_income_account.balance + @amount) 37 38 } 39 #完全成功なら、root_path(ホーム画面へ) 40 redirect_to root_path 41 #以下は例外が発生(プロセスの内どこかが失敗)したときに行う。rescueは自分でカスタマイズした例外処理を行えるが、railsが自動的に表示してくれるエラーを見たければ一旦resucueをコメントアウトしてみよう。 42 rescue e.message 43 #flash[:notice] = "失敗しました。リトライしてみてください" 44 #render "new"    45 #rollback 46 end 47 end

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

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

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

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

ruby

1begin 2 ActiveRecord::Base.transaction { 3 モデルA.save! 4 5 # 途中でなんらかの条件によりエラーと判定したい場合 6 # rollbackしたいので、例外を自分で発生させる 7 if xxxx 8 raise "[error] rollback!" 9 end 10 11 モデルB.save! 12 モデルC.save! 13 } 14rescue Exception => e 15 # rollback 16end

投稿2017/12/17 14:53

mingos

総合スコア4025

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

zendendo

2017/12/19 07:17 編集

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問