【Rails】標準機能ではできないバリデーションをカスタムメソッドで解決!

はじめに

こんにちは株式会社のナオキです!
今回はRailsのバリデーションに関する記事になります。

バリデーションはアプリケーションの品質を保つ為の重要な要素です。
基本的な文字数制限等のバリデーションであればRailsの標準機能で対応できますが、それでは対応できないケースに出会うことがあります。
そのような場合にカスタムメソッドを活用し、要件を満たす方法を紹介します。

この記事では以下の内容について取り上げています。

  • Railsの標準バリデーションの紹介
  • カスタムメソッドの必要性
  • ユースケースごとの実装例

対象読者

  • Rails初学者の方

Railsの標準バリデーションの紹介

空でないことを検証(presence)

属性の値が空でないことを確認します。

validates :title, presence: true

一意性の検証(uniqueness)

属性の値が一意であることを確認します。

validates :email, uniqueness: true

フォーマットの検証(format)

属性の値が指定した正規表現と一致するか確認します。
今回withで指定している正規表現はメールアドレス用フォーマットの正規表現になります。

validates :email, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i }

数値であることを検証(numericality)

属性の値が数値のみか確認します。
greater_thannumericalityのオプションで指定した数値より大きいかを確認しています。

validates :price, numericality: { greater_than: 0 }

値の長さを検証(length)

属性の値が文字列で指定された文字数かどうかを確認します。
minimum, maximumlengthのオプションで最低値と最高値を指定しています。

validates :password, length: { minimum: 6, maximum: 20 }


他にもバリデーションとオプションが多数あるので気になる方は以下を参照してください!

カスタムメソッドの必要性

Railsには上記のような便利なバリデーションが標準機能としてあります。
基本的なバリデーションは標準機能で事が済みますが、複雑な要件を満たすためには、独自のバリデーションが不可欠になってきます。
それらをユースケースとともに紹介していきます!

ユースケースごとの実装例

複数のカラムを組み合わせたバリデーション

標準のバリデーションでは、2つ以上の属性の関係性を考慮することができません。
なので、カスタムメソッドで実装していきます。

例:開始日と終了日を設定する場合に、「終了日が開始日よりも後でなければいけない」というバリデーションを追加したい。

class Reservation < ApplicationRecord
  validate :end_date_after_start_date

  def end_date_after_start_date
    if end_date <= start_date
      errors.add(:end_date, "終了日は開始日より後の日付を選択してください")
    end
  end
end

if end_date <= start_dateで、終了日が開始日より後かの判定をしています。
errors.addを使用して、終了日が開始日と同日かそれ以前の場合のエラーメッセージを設定しています。
同日かそれ以前だった場合に、「終了日は開始日より後の日付を選択してください」とエラーメッセージが表示されます。

他のモデルのデータと比較するバリデーション

標準のバリデーションでは、他のモデルのデータとの比較することができません。

例:注文する場合に、「注文数が在庫数を超えていたら注文できない」というバリデーションを追加したい。

class Order < ApplicationRecord
  validate :stock_must_sufficient

  def stock_must_sufficient
    if quantity > product.stock
      errors.add(:quantity, "注文数が在庫数を超えています。")
    end
  end
end

if quantity > product.stockで、注文書が在庫数を超えてるかの判定をしています。
超えていた場合に、「注文数が在庫数を超えています」とエラーメッセージが表示されます。

他のレコードと比較が必要なバリデーション

標準のバリデーションでは、他のレコードの情報を参照できません。

例:レンタルスペースで、「各部屋は1つの時間帯に1つの予約しかできない」というバリデーションを追加したい。

class Reservation < ApplicationRecord
  validate :no_time_overlap

  def no_time_overlap
    if Reservation.where(room_id: room_id)
                  .where.not(id: id)
                  .where("start_time < ? AND end_time > ?", end_time, start_time)
                  .exists?
      errors.add(:base, "この時間帯はすでに予約されています")
    end
  end
end

.where(room_id: room_id)で部屋の情報を取得し、.where.not(id: id)で自分自身のidを除外しています。予約を更新する場合、自分自身のidを考慮していないとエラーが発生するからです。
.where("start_time < ? AND end_time > ?", end_time, start_time)で他の予約との時間が被っていないかを確認しています。
最後に.exists?で条件に合うレコードが存在したらtrueを返し、「この時間帯はすでに予約されています」とエラーメッセージが表示されます。

おわりに

今回はRailsのカスタムメソッドでのバリデーションについての紹介をしました。
大体のバリデーションは標準機能で実装できますが、複雑な要件などでは実装できない場合もあります。
そのような場合は、独自のカスタムメソッドを作成し、バリデーションを柔軟に実装していきましょう!