【Rails】複雑なバリデーションはメソッドにしてModelに書いておく
自作アプリでちょっと複雑なバリデーションを書いたので書き残しておきます。
プログラミング初学者のアウトプットメモになります。情報に誤りがあればご指摘ください。
やりたいこと
ほしいものリストアプリをつくる
ユーザーは自分の登録したアイテムを比較できる
比較は保存できて、以前に比較した内容も見れる
ユーザーは自分の比較一覧を見ることができる
必要な制限
ユーザーがアクセスできるアイテムは「自分のアイテム」のみ
ユーザーが比較できるアイテムは「自分のアイテム」のみ
ユーザーがアクセスできる比較は「自分の比較」のみ
関係性
ユーザーは、複数のアイテムを持ち(has_many :items)、アイテムは必ずユーザーに属す(belong_to :user)
ユーザーは、複数の比較を持ち(has_many :comparisons)、比較は必ずユーザーに属す(belong_to :user)
比較は複数のアイテムを持つが、アイテムは複数の比較に属することができる、多対多のアソシエーション
(これって中間テーブルっぽい振る舞いをしながらも単体でしっかり役割を持っているんですが、アンチパターンなんですかね?)
比較(comparison)に関するバリデーション
primary_item_id として、itemsテーブルのitem_idを持ってくる
secondary_item_idとして、itemsテーブルのitem_idを持ってくる
ユーザーに属する
primary_item_idとsecondary_item_idには、同じIDが登録できない
すでにある組み合わせは新規登録できない
他のユーザーが持っているアイテムのIDは比較に登録できない
難しいところ
上記の4~6は、ちょっと複雑なバリデーションです。
(「100文字以内でないといけない」とか、「空欄は禁止」とかそういうのと比較したら、複雑)
やったこと
model内のprivateメソッドで条件式を記述する
メソッドには「こうなってたらNG!」という状況を式で記述する
エラーメッセージを記述しておく
実際のコード
class Comparison < ApplicationRecord
belongs_to :primary_item, class_name: 'Item', foreign_key: 'primary_item_id'
belongs_to :secondary_item, class_name: 'Item', foreign_key: 'secondary_item_id'
belongs_to :user
has_many :notes, dependent: :destroy
validate :different_items
validate :unique_combination
validate :items_belong_to_user
private
def different_items
if primary_item_id == secondary_item_id
errors.add(:secondary_item_id, "はアイテム1と異なるものを選択してください")
end
end
def unique_combination
if Comparison.exists?(primary_item_id: primary_item_id, secondary_item_id: secondary_item_id) ||
Comparison.exists?(primary_item_id: secondary_item_id, secondary_item_id: primary_item_id)
errors.add(:base, "このアイテムの組み合わせはすでに存在します")
end
end
def items_belong_to_user
if primary_item_id.present? && secondary_item_id.present?
if primary_item.user.id != user.id || secondary_item.user.id != user.id
errors.add(:base, "比較するアイテムはユーザーが所有しているものでなければなりません")
end
end
end
end
different_itemsメソッド
もし1個目のアイテムと2個目のアイテムが同じだったらエラーを出すメソッド。
unique_combinationメソッド
すでにある組み合わせが作成された場合、新規作成しないでエラーを出すメソッド。かっちょいいことに、組み合わせが前後逆になっても対応できる。
このバリデートとは別に、コントローラー側で「すでに存在する組み合わせの比較を作成しようとしたら、すでに作成済みの比較の画面に飛ばす」という挙動をセットしています。実際にユーザーがこのメッセージを見るわけではありません。(下記抜粋)
class ComparisonsController < ApplicationController
def create
existing_comparison = find_existing_comparison
if existing_comparison
redirect_to comparison_path(existing_comparison)
else
@comparison = Comparison.new(comparison_params)
if @comparison.save
redirect_to @comparison
else
user_items_not_purchased
render :new, status: :unprocessable_entity
end
end
end
private
def find_existing_comparison
Comparison.find_by(primary_item_id: comparison_params[:primary_item_id], secondary_item_id: comparison_params[:secondary_item_id]) ||
Comparison.find_by(primary_item_id: comparison_params[:secondary_item_id], secondary_item_id: comparison_params[:primary_item_id])
end
items_belong_to_userメソッド
ユーザーが持っていない、他人のアイテムを勝手に比較登録できないようにするメソッド。
これも普通に使っていると発生しないエラー。
もともと、UI上ではユーザーが持っているアイテムしか選択・選択できないので、普通のユーザーがこのバリデーションに引っかかることはありません。
コンソールから直接item_idを指定した場合などに誤って他人のアイテムを比較しないようにするためのバリデーションです。
バリデーションは画面操作上の制限とは別で入れとこう
上記の通り、ふつうに使っていると「自分のアイテムしか見られないユーザーが比較を作成する」のだから、「他人のアイテムを比較に突っ込む」ことはないはずです。
しかし、まぁ画面の操作だけが全てではありませんし、何があるかわかりません。フォームからなんか変な操作をすれば、なんかの隙をついて他人のデータを書き換えたりできてしまう可能性もあるかもしれません。
また、万が一フロントの実装がミスっていて他人の情報がちらっと見えかけてしまっても、他人の情報は操作できないようにする制限をかけておけば助かるデータやプライバシーがあるかもしれません。URL直打ちで…とかね。
ということで、「UI上ではあり得ない挙動でも、一応制限をかけておく」のが良さそうです。たぶん。
今日は以上です。最後までお読みいただきありがとうございました。
この記事が気に入ったらサポートをしてみませんか?