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

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

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

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

Ruby on Rails

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

Q&A

解決済

2回答

1684閲覧

他のテーブルの値をvalidates(検証)の一つとして利用したい

zendendo

総合スコア43

Ruby

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

Ruby on Rails

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

0グッド

0クリップ

投稿2017/12/05 08:06

編集2017/12/06 23:37

###前提・実現したいこと
ruby(ruby on rails)で、ポイント振替機能みたいなものをつくっていているのですが、
2つの問題が起きてしまっています。
一つ目は残高以上の数値を入力しても取引が実行されてしまうので、
モデルのvalidates(検証)で、支払ユーザー(現在ログインしているユーザー)が保有する口座残高以下の取引額のみ検証成功」にしたいと思っています。

ただ、残高を記録するモデルと取引額を記録するモデルは別々に分かれています。
図にするとこんな感じです。

イメージ説明
例えば、AccountTransactionモデルにamount(取引額)を記録する前に、ログインユーザーの保有する残高が記録されたBasicIncomeAccountモデルのbalance(残高)を参照して、
「入力された値が残高以下なら検証成功として記録」
「入力された値が残高以上なら検証失敗として記録しない」
ようにしたいのですのですが、やり方がわからず困っています。

2つ目の問題は、入力フォームでマイナスの取引額を入力してしまうと、出金処理と入金処理が逆転してしまうので、「入力フォームでマイナス数値の入力を不可」にしたいと思っているのですが、フォーム側で制限ってできるのでしょうか?それとも、モデル側で制限した方がいいのでしょうか?

教えて頂ければ幸いです。

###現状のコード
入力フォームです。
ユーザーは、口座番号と送金額だけ入力しています。
app/views/account_transactions/new.html.erb

<%= form_for @account_transaction do |f|%> <div class="field"> <%= f.label :送金相手の口座番号 %> <%= f.text_field :deposit_account_id %> <%= f.label :送金額 %> <%= f.number_field %>  <%= f.submit"送金確認" %> </div> <% end %>

account_transactions(取引)テーブル、
basic_income_accounts(口座)テーブル、
の内容です。

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

また、BasicIncomeAccount(口座モデル)AccoutTransaction(取引モデル)は、
1:多の関連付けが施されています。

app/models/basic_income_account.rb

class BasicIncomeAccount < ApplicationRecord belongs_to :user validates :user_id, presence:true validates :account_number, presence: true, uniqueness: true validates :balance, presence: true, numericality: true #BasicIncomeAccountは、AccountTransactionを多数保有してる1:多の関係である 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' has_many :deposits_and_withdrawals

今回の問題の本題となる取引モデルのコードです。
app/models/account_transaction.rb

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, if: :balance_check has_many :deposits_and_withdrawals  private ##おそらくカスタムなバリデーションをつくることになるが、その記述方法がわからない def balance_check if current_user.basic_income_acccount.balance < ?????????? errors.add(:amount, "残高以上の送金はできません") end end end

追記
account_transactionsのコントローラです。
ここで、AccountTransactionとDepositsAndWithdrawalにレコードを作成し、
BasicIncomeAccountの更新を同時かつトランザクション処理でできるようにしております。
(ポイント振替処理ができるようになっています)
/app/controllers/account_transactions_controller.rb

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! 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! 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]) @withdrawal_amount = params[:account_transaction][:amount].to_f @deposit_amount = params[:account_transaction][:amount].to_f @withdrawal_basic_income_account.update(balance: @withdrawal_basic_income_account.balance - @withdrawal_amount) @deposit_basic_income_account.update(balance: @deposit_basic_income_account.balance + @deposit_amount) end end end #トランザクション処理が完全成功なら、root_path(ホーム画面)へ飛ぶ redirect_to root_path #トランザクション処理で例外発生(失敗)したら、以下の例外処理を実行する rescue => e render plain: e.message

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

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

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

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

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

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

guest

回答2

0

ベストアンサー

AccountTransaction の amount に関する検証ですけど

ruby

1class AccountTransaction < ApplicationRecord 2 belongs_to :withdrawal, class_name: 'BasicIncomeAccount', foreign_key: 'withdrawal_account_id' 3 validates :amount, presence: true, numericality: {greater_than: 0} 4 validates :amount_cannot_be_greater_than_balance 5 6 def amount_cannot_be_greater_than_balance 7 if amount > withdrawal.balance 8 errors.add(:amount, "残高以上の送金はできません") 9 end 10 end

折角 AccountTransaction に belongs_to :withdrawal にて BasicIncomeAccount とのリレーションを用意してあるので,単純に amount > withdrawal.balance のチェックではいかがでしょうか.

加えて

ruby

1@withdrawal_basic_income_account.update(balance: @withdrawal_basic_income_account.balance - @withdrawal_amount)

ここで,残高を超えたポイント引き落としが行われないように

ruby

1class BasicIncomeAccount < ApplicationRecord 2 validates :balance, presence: true, numericality: {greater_than_or_equal_to: 0}

などの制約を加えておくのはいかがでしょうか.

使ったことはないのですが,数値に関して制約が付けれるようです.
Active Record バリデーション 2.8 numericality

2つ目の問題は、... フォーム側で制限ってできるのでしょうか?それとも、モデル側で制限した方がいいのでしょうか?

基本的に両方で制約をつけるのがよいかと思います.フォーム側で制限した方がユーザフレンドリーですし,データベースの一貫性を考えればモデル側でもきちんと制限しないといけないですし.

最後に,フォーム側での制限については Form Object を作る案に賛成です.

投稿2017/12/19 05:21

togino77

総合スコア143

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

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

zendendo

2017/12/19 07:11

togino77さん、回答して頂きありがとうございます。 アドバイス通りにカスタムなバリデーションを作成することで、他のモデルの値をバリデーションとして使うことができました。 何日も詰まってしまっていたので本当に助かりました!
guest

0

まず、問題1についてですが、複数のモデルに関連するバリデーションを1つのモデル内で行うのは止めた方がよさそう。
デザインパターンの 'Form Object' を使ってみてはいかがでしょう。検索してみてください。

例) ※ちょっと適当すぎますがよしなに

class AccountTransactionForm include ActiveModel::Model attr_accessor :errors, :amount, :deposit_account_id def initialize(amount, deposit_account_id, basic_income_acccount) @amount = amount @deposit_account_id = deposit_account_id @balance = basic_income_acccount.balance @errors = ActiveModel::Errors.new(self) end def valid?(zip = nil) errors.clear if @balance < amount errors.add(:amount, "残高以上の送金はできません") end errors.empty? end end

問題2についてですが、
<%= f.number_field :amount, pattern: '[1-9][0-9]*' %>  とかでできないですかね。(未検証)

投稿2017/12/06 01:08

編集2017/12/06 01:09
退会済みユーザー

退会済みユーザー

総合スコア0

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

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

zendendo

2017/12/06 23:20 編集

_RYO_さん、回答して頂きありがとうございます! Form Objectなるものは初めて知ったので、調べてみます! ひとまず途中経過報告を。 2つ目の問題「フォームにマイナス入力をさせない」については、 入力フォームのviewファイルのamount入力欄の記述を <%= f.number_field :amount,min:1,max:999999999999999 %> とすることで解決することができました。 そして一つ目の問題「支払口座の残高以上の取引を無効にする」についてはまだ解決できておりません。 現状では、app/models/form/account_transaction_form.rbに 上記のコードを記述しましたが、 コントローラ側にも書き換えを行う必要があるということでしょうか?
退会済みユーザー

退会済みユーザー

2017/12/07 00:56

そうですね。Controller側からForm Object を呼ぶ感じです。 参考) http://tech.medpeer.co.jp/entry/2017/05/09/070758 イメージとしては、Modelを跨ったの複雑な処理やバリデーションを `Form Object` 内で行います。 前回の例で抜けてますが、 複数の登録処理をController側でやっている場合は `save` も実装した方が Controller が見やすくなりますね。 ``` class AccountTransactionForm include ActiveModel::Model attr_accessor :errors, :amount, :deposit_account_id def initialize(amount, deposit_account_id, basic_income_acccount) @amount = amount @deposit_account_id = deposit_account_id @balance = basic_income_acccount.balance @errors = ActiveModel::Errors.new(self) end def save return false unless valid? # 複数のModelを使用する保存処理 true end def valid? errors.clear if @balance < amount errors.add(:amount, "残高以上の送金はできません") end errors.empty? end end ``` あと私事ですが業務が忙しくなってきたので今後コメントできないです。あとは自力でがんばってください。
zendendo

2017/12/07 01:38

_RYO_さん、お忙しいところありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問