Webアプリでvalidationを書いていると、DBに問い合わせをしなければいけない時があるのですが、その時に、値が変更されているかチェックしてからDB問い合わせした方がよいよというお話です。値が変更されているかは、Railsではwill_save_change_to_#{attr_name}?
というメソッドが用意されています。#{attr_name}
の部分はModelの属性(カラム名)で置き換えます。
値が変更されているか調べてからvaldiateする
前回に引き続き、会議室予約アプリを例にして、具体的な使い方をみてみます。
会議室(meeting_room)は複数の会議予約(meeting)をもちます。会議予約は、開始時刻(start_time)と終了時刻(end_time)をもちます。
★前回の記事
仕様
- 会議予約(前回実装済み)
- subjectは32文字以下で必須項目
- start_timeは必須項目
- end_timeは必須項目
- end_timeはstart_timeより後であること
- 新規登録時、start_timeは現在時刻より後であること
- start_timeが変更された時、start_timeは現在時刻より後であること
- 保存済みのstart_timeが現在より前の場合は、start_timeを変更できない
- 保存済みのstart_timeが現在より前の場合は、end_timeを変更できない
これに、
- 会議予約
- 同一の会議室に属するmeetingの期間は、重複してはならない。つまり、ある時刻に1つの会議室で開催されている会議は最大で1つである。
というvalidationを追加します。
コード
仕様を満たすコード
class Meeting < ApplicationRecord # 略 validate :meeting_time_should_not_overlap private # 略 def meeting_time_should_not_overlap return unless start_time && end_time if Meeting .where(meeting_room_id: meeting_room.id) .where('start_time < ?', end_time) .where('end_time > ?', start_time) .where.not(id: id).exists? errors.add(:base, '他の会議と重複しています。') end end end
Meeting.where(meeting_room_id: meeting_room.id).where('start_time < ?', end_time).where('end_time > ?', start_time)
で登録しようとする会議予約と同じ会議室の中で、開始終了時間と重複するレコードを検索します。直感的にわかりづらい条件ですが、これで検索できます。
続くwhere.not(id: id)
で、更新対象のレコードを除外しています。除外しないと対象レコードも検索に引っかかってきてしまうためです。最後にexsists?
をつけて、存在するかどうかだけを確認しています。
生成されるSQLは以下のようになります。
SELECT 1 AS one FROM "meetings" WHERE "meetings"."meeting_room_id" = ? AND (start_time < '2018-08-10 12:00:00') AND (end_time > '2018-08-10 11:00:00') AND "meetings"."id" != ? LIMIT ? [["meeting_room_id", 1], ["id", 3], ["LIMIT", 1]]
このコードで問題なく動くのですが、ひとつ気になるのは、このSQLは、validationのたびに実行されるということです。例えば、subjectしか更新しないようなリクエストでも毎回重複チェックのためのSQLが実行されます。
一般的にSQLの実行はコストが高いため、必要なとき以外は実行しないようにコードを書き換えます。
値が変更された場合のみDBアクセスをするコード
validationが必要なのは、登録時と開始時間または終了時間が変更されたときです。
class Meeting < ApplicationRecord # 略 validate :meeting_time_should_not_overlap, if: :new_or_start_or_end_time_changed private # 略 def new_or_start_or_end_time_changed new_record? || will_save_change_to_start_time? || will_save_change_to_end_time? end def meeting_time_should_not_overlap return unless start_time && end_time if Meeting .where(meeting_room_id: meeting_room.id) .where('start_time < ?', end_time) .where('end_time > ?', start_time) .where.not(id: id).exists? errors.add(:base, '他の会議と重複しています。') end end end
new_record?
(登録時)、will_save_change_to_start_time?
(start_timeが変更されているとき)、そしてwill_save_change_to_end_time?
(end_timeが変更されているとき)にのみSQLを実行するように変更できました。
実行結果
現在の予約状況
他の会議室101の会議時間を重複する会議は登録できない↓
他の会議室101の会議と重複しない会議は登録できる↓
他の会議室101の会議時間を重複する日時に変更できない↓
他の会議室101の会議と重複しない会議は登録できる↓
日時が変更されない(subjectのみ変更)場合は、重複チェックのSQLが実行されない↓
環境
参考
Rails4とRails5ではDBの値をとるメソッドや変更されたかを取得するメソッドの名前が変更されています。rail5の方がわかりやすいメソッド名ですね。#{attr_name}_in_database
と will_save_change_to_#{attr_name}?
以外のメソッドもこちらにまとまっています。