Ruby on Rails

【Rails】さまざまなバリデーション

おはようございます。今日は金曜日で一週間も終わりですが、変わらずコツコツやっていきます。
今回は、Railsのバリデーション機能についてよく分からなりググっているので、それについてまとめていければと思います。

なぜバリデーションをつけるのか

バリデーションをつけないと意図しないデータがデータベース上に入ってしまい、アプリケーション上でエラーを引き起こしてしまったり、脆弱性を入れ込んでしまう原因にもありえます。DBの制約を利用する方法もありますが、入力に近いアプリケーション側でバリデーションを行い、不正なデータを弾くのがベターです。

さまざまなバリデーション

最初にパーフェクトRuby on Railsでの内容をまとめていきます。気になった方はぜひ手にとって読んでみてください。少し難しいですが、すごく勉強になります。
下のようなbooksテーブルが定義されているとします。

booksテーブル
name
price
published_on
sales_status
publisher_id

それでは、Bookモデルにバリデーションを書いていきます。アプリケーション上の要件として以下のものが挙げられたとしましょう。

  • 本の名前は必ず入力されていること
  • 本の名前は25文字以下であること
  • 本の価格は0円以上であること

このような単純な要件であれば、ActiveRecord / ActiveModelの機能を用いれば簡潔に書くことができます。

class Book < ApplicationRecord
  validates :name, presence: true
  validates :name, length: { maximum: 25 }
  # 連ねて書くこともできる
  # validates :name, presence: true, length: { maximum: 25 }
  validates :price, numericality: { greater_than_or_equal_to: 0 }
end

また、他にもActiveModelには様々なバリデーション機能が備わっています。

class Book < ApplicationRecord
  # 3字以上で入力
  validates :name, length: { minimum: 3 }

  # 1字以上10字以下を許可
  validates :nickname, presence: true, length: { in: 1..10 }

  # 301円から99,999円を許可
  validates :price, numericality: { grater_than: 300, less_than_or_equal: 99,999 }

  # 数字のみ許可
  validates :price, numericality: { only_integer: true }

  # 関連先のモデルのバリデーションチェックも行う
  validates_associated :publisher
end

class User < ApplicationRecord
  # ユニーク制約
  validates :email, uniqueness: true

  # フォーマットチェック
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }

  # チェックボックスにチェックがついているか確認 (※)
  validates :terms_of_service, acceptance: true
end

class Like < ApplicationRecord
  # 複合ユニーク制約
  validates :user_id, uniqueness: { scope: :post_id }
end

※ acceptanceメソッドは、フォームが送信されたときにチェックボックスがオンになっているかどうかを検証します。ユーザーによるサービス利用条項への同意が必要な場合や、ユーザーが何らかの文書に目を通したことを確認させる場合によく使われます。これはデータベースに保存する必要はありません。これに対応するフィールドがなくても、単にヘルパーが仮想の属性を作成してくれます。(Railsガイドより)

さかい
さかい
よく使うバリデーションを挙げておいたよ

カスタムバリデーション

この他にも自分でバリデーションをカスタムしたい場合もあるかと思います。その場合はvalidateブロックを利用すると簡単に実装ができます。
errorsオブジェクトに何かしらの値が入っていれば、バリデーションエラーとみなされるため、自分でバリデーションを定義する場合には不正な条件であればerrorsにエラーメッセージを付与するという形がいいでしょう。

validate do |book|
  if book.name.include?("exercise")
    book.errors[:name] << "I don't like exercise."
  end
end

コンソールで挙動を確認します。

publisher = Publisher.first
book = Book.new(name: "exercise", price: 1000, publisher: publisher)
book.valid?
=> false
book.errors.messages
=> {:name=>["I don't like exercise."]}

また、以下のようにプライベートメソッドを渡す書き方もできます。

validate :email_is_not_taken_another

private

def email_is_not_taken_another
  errors.add(:email, :taken, value: email) if User.exists?(email: email)
end

上で使っているaddメソッドは特定の属性に関連するエラーメッセージを手動で追加できます。このメソッドは属性とエラーメッセージを引数として受け取ります。ここでは、エラーメッセージとしてtakenが引数として渡されていますが、これは"has already been taken"というメッセージになります。

https://railsguides.jp/active_record_validations.html#errors-add

エラーメッセージが重複して表示されるのを防ぐ方法

また、モデルで下記のようにバリデーションをはっていた場合、エラーメッセージが重複して表示されることがあるかと思います。

class User < ApplicationRecord
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end

emailが空で送られてくると、次のようにエラーメッセージが複数格納されており、これを表示させるとエラー文が重複して表示されてしまいます。

self.valid?
=> false
self.errors.messages
=> {:email=>["can't be blank", "is invalid"]}

これを防ぐには、allow_blank: trueオプションをつけ、emailが空欄の場合はバリデーションをスキップするようにします。

class User < ApplicationRecord
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP, allow_blank: true }
end

すると、もう一度emailを空で送信をすると、エラーメッセージが重複しないのが確認できると思います。

self.valid?
=> false
self.errors.messages
=> {:email=>["can't be blank"]}

2つのテキストフィールドで受け取る内容が完全に一致するか確認する方法

少し長くなりましたが、タイトルのようなことをしたい場合、confirmation: trueとし、テキストフィールドとして***_confirmationフィールドを用意します。emailならemail_confirmationとなります。

class User < ApplicationRecord
  validates :email, confirmation: true
end
# 略
  <%= form_with model: @user_registration_form, url: user_registrations_path do |f| %>

    <%= f.label :email %>
    <%= f.text_field :email %>

    <%= f.label :email_confirmation, "Confirmation" %>
    <%= f.text_field :email_confirmation %>
# 略

すると、userモデルに仮想的なemail_confirmation属性がつくられ、バリデーションがはられます。試しに下画像のようにemailとemail_confirmationを異なる値にして送信してみます。

すると、errorsオブジェクトに値が入っているのが確認できます。

self.valid?
=> false
self.errors.messages
=> {:email_confirmation=>["doesn't match Email"]}

さいごに

いかがだったでしょうか。バリデーションの種類はまだまだたくさんあって全部覚えるのは不可能なので、必要になったらRailsガイドを見るのがいいかと思います。また、よく使うバリデーションがあればコメント欄などで教えていただけると嬉しいです。ここまで読んでいただきありがとうございました。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!