Railsで値が変更されているか調べてvaldiateする

Webアプリでvalidationを書いていると、値が変更になった時にだけvalidationをかけたい場合があります。Railsではwill_save_change_to_#{attr_name}?というメソッドが用意されています。#{attr_name}の部分はModelの属性(カラム名)で置き換えます。

値が変更されているか調べてからvaldiateする

会議室予約アプリを例にして、具体的な使い方をみてみます。

会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。

f:id:yucatio:20180809214216p:plain

ルーティングはこちらです。

# routes.rb
Rails.application.routes.draw do
  resources :meeting_rooms do
    resources :meetings
  end
end

仕様

  • 会議予約
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • start_timeはend_timeより前であること
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

コード

本題ではない方のvalidation
  • 会議予約
    • subjectは32文字以下で必須項目
    • start_timeは必須項目
    • end_timeは必須項目
    • start_timeはend_timeより前であること

このへんは今回の本題ではないのでコードだけ載せておきます

class Meeting < ApplicationRecord
  belongs_to :meeting_room

  validates :subject, length: {maximum: 32}, presence: true
  validates :start_time, presence: true
  validates :end_time, presence: true

  validate :start_time_should_be_before_end_time

  private
  def start_time_should_be_before_end_time
    return unless start_time && end_time

    if start_time >= end_time
      errors.add(:start_time, 'は終了時間よりも前に設定してください')
    end
  end
end

値の変更時にvalidationをかける

  • 会議予約
    • 新規登録時、start_timeは現在時刻より後であること
    • start_timeが変更された時、start_timeは現在時刻より後であること

こちらを実装したコードは以下です。

class Meeting < ApplicationRecord
  # 略

  validate :start_time_should_be_after_current, if: :new_or_start_time_changed

  private
  # 略

  def new_or_start_time_changed
    start_time.present? && (new_record? || will_save_change_to_start_time?)
  end

  def start_time_should_be_after_current
    if start_time <= Time.zone.now
      errors.add(:start_time, 'は現在時刻よりも後に設定してください')
    end
  end
end

new_or_start_time_changedメソッドの中で、 will_save_change_to_start_time?を使うことによって、start_timeが変更されたかどうかを判定しています。

start_time.present? && (new_record? || will_save_change_to_start_time?)は、start_timeがnilでないかつ、新規またはstart_timeの値が変更されている、という意味です。

if: :new_or_start_time_changedを外して、

class Meeting < ApplicationRecord
  # 略

  validate :start_time_should_be_after_current

  private
  # 略

  def start_time_should_be_after_current
    if start_time <= Time.zone.now
      errors.add(:start_time, 'は現在時刻よりも後に設定してください')
    end
  end
end

と書いてしまうと、start_timeが過去になってしまうと、全ての項目(subjectなど)が更新できなくなってしまうので注意してください。

実行結果

新規登録時

※実行時は2018年8月10日

開始時間に過去の日時は登録できない↓

f:id:yucatio:20180811152352p:plain

開始時間に未来の日時は登録できる↓

f:id:yucatio:20180811152403p:plain

更新時

※実行時は2018年8月10日

開始時間が過去で登録されていて、日時が変更無しのとき(subjectのみ変更)は更新できる↓

f:id:yucatio:20180811154046p:plain

開始時間が過去の場合、未来の日時へ変更できる↓

f:id:yucatio:20180811154934p:plain

開始時間が過去の場合、別の過去の日時には変更できない↓

f:id:yucatio:20180811155634p:plain

開始時間が未来の場合、 過去の日時には変更できない↓

f:id:yucatio:20180811160509p:plain

開始時間が未来の場合、別の未来の日時には変更できる↓

f:id:yucatio:20180811161128p:plain

環境

続く

こちらもどうぞ

yucatio.hatenablog.com

yucatio.hatenablog.com

参考

Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_databasewill_save_change_to_#{attr_name}? 以外のメソッドもこちらにまとまっています。

qiita.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

Ruby on Rails 5アプリケーションプログラミング [ 山田祥寛 ]
価格:3888円(税込、送料無料) (2018/8/9時点)