はじめに
前回は
バリデーションメッセージのテンプレート化 として
- コンソール
- バリデーション
- errors
- バリデーションメッセージ
について学びました。
今回は
Enumや日時のバリデーション として
- Enumのバリデーション
- カスタムバリデーション
- 日時のバリデーション
- タイムゾーン
について学びます。
では、はじめていきましょう。
1. バリデーションヘルパー
ヘルパー一覧
バリデーションに関しては、かなりの項目数になるので、一度Railsガイドに目を通してみると良いでしょう。
今回は、Rails7以降で追加された便利なバリデーションを含めてやっていきましょう。
2. Enumのバリデーション
validate: true
Enumの validate
オプションはRails7.1から導入されたようです。
Enumは決められた値以外のものを割り当てると、例外エラーとなってしまいますので、これを防ぐ役割があります。
では、コンソールで検証してみましょう。
todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001>
でコンソールを起動します。
irb(main):001> Task.new
=>
#<Task:0x00000001100d9380
id: nil,
title: nil,
description: nil,
start_time: nil,
end_time: nil,
status: "not_started",
created_at: nil,
updated_at: nil>
空でインスタンスを作ると、 status
にデフォルト値が入った状態で作成されました。
次にenumが想定していない not_started
in_progress
closed
以外の値を入れてみます。
irb(main):002> Task.new(status: :hello)
/opt/local/asdf/installs/ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.1.1/lib/active_record/enum.rb:215:in `assert_valid_value': 'hello' is not a valid status (ArgumentError)
raise ArgumentError, "'#{value}' is not a valid #{name}"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
例外エラーとなりました。 enum
のHashが参照出来ないようです。
もしも何かのミスで想定しない値が入った場合に例外ではなくバリデーションで拾いたいので、 enum
の記述に少し追記します。
Taskモデルを見ましょう。
app/models/task.rb【確認】
class Task < ApplicationRecord
enum status: { not_started: 0, in_progress: 1, closed: 2 }
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true
end
Enumの場合、 validates
メソッドを使わずに enum
を使いますので、
enum status: { not_started: 0, in_progress: 1, closed: 2 }
この行を、
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true
と書き換えます。 :status
の部分の書き方も変わっているので注意して下さい。
app/models/task.rb【編集】
class Task < ApplicationRecord
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true #←ここ
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true
end
書き足したら、再読み込みを行います。
irb(main):003> reload!
Reloading...
=> true
次に、もう一度先程のコードを実行します。
irb(main):004> Task.new(status: :hello)
=>
#<Task:0x000000010565f8c8
id: nil,
title: nil,
description: nil,
start_time: nil,
end_time: nil,
status: :hello,
created_at: nil,
updated_at: nil>
今度はエラーになりませんでした。(*再び例外エラーになった場合は一旦コンソールを抜けて再起動してみて下さい。)
バリデーションをチェックしましょう。
irb(main):005> task = Task.new(title: "test", start_time: Time.current, status: :hello)
=>
#<Task:0x0000000106751690
...
これで starus
だけが不正な値のはずです。
irb(main):006> task.valid?
=> false
irb(main):007> task.errors.full_messages
=> ["進捗は一覧にありません"]
想定通りとなりました。
3. カスタムバリデーション
validate
Rails7.1 になって益々便利になりましたが、それでもバリデーションを自分で書きたい場合等もあります。
そういった場合は、 validate
を使います。
書き方は、
validate :対象カラム名, :自作のメソッド名
となります。
例えば start_time
は必須入力にはなっていますが、日付そのものには制約がありません。
app/models/task.rb【確認】
class Task < ApplicationRecord
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true
end
これに 現在時刻の日のスタート時、0:00からしか入力出来ない
バリデーションを付けたいとなった場合、
def only_after_today
today = Time.current.beginning_of_day
if today > start_time
errors.add(:start_time, "は#{today}より大きい値にしてください")
end
end
このようなメソッドを作って、条件にあてはまる場合のみ errors
にメッセージを追加します。
validate :start_time, :only_after_today
validates
で呼び出します。
編集後は、以下のようになります。
app/models/task.rb【編集】
class Task < ApplicationRecord
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true
#ここから追加
validate :start_time, :only_after_today
private
def only_after_today
today = Time.current.beginning_of_day
if today > start_time
errors.add(:start_time, "は#{today}より大きい値にしてください")
end
end
#ここまで追加
end
また、 I18n も使えますので、
"は#{today}より大きい値にしてください"
の部分は、
"は#{I18n.l(today, format: :long)}より大きい値にしてください"
などのようにすることも可能です。
では検証します。
todo_app % rails c
でコンソールを起動します。
title
だけに値を入れておきましょう。
irb(main):008> task = Task.new(title: "test")
=>
#<Task:0x0000000108c74580
...
次に、
start_time` に現在時刻から遡った前日の値を入れます。
irb(main):009> task.start_time = Time.current.yesterday
=> Mon, 20 Nov 2023 05:17:45.074460000 UTC +00:00
バリデーションを試します。
irb(main):010> task.valid?
=> false
irb(main):011> task.errors.full_messages
=> ["開始日は2023-11-21 05:15:46 UTCより大きい値にしてください"]
通りませんでしたが、時間を入れ直すと通ります。
irb(main):012> task.start_time = Time.current
=> Tue, 21 Nov 2023 05:24:27.811528000 UTC +00:00
irb(main):013> task.valid?
=> true
これで、start_time
は現在時刻よりも前に登録できなくなりました。
4. 日付のバリデーション
comparison
せっかくカスタムバリデーションを書いたのですが、日付においてはもう少し便利になったようです。
これもRails7から導入されたオプションとなりますが、イメージ的には :numericality
に近い使い方で記述する事ができます。
comparison
を使って、同じ事を実現してみましょう。
Taskモデルは、先程編集したので以下の通りです。
app/models/task.rb【確認】
class Task < ApplicationRecord
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true
validate :start_time, :only_after_today
private
def only_after_today
today = Time.current.beginning_of_day
if today > start_time
errors.add(:start_time, "は#{today}より大きい値にしてください")
end
end
end
これに、 comparison
を使うと以下のようにすっきりします。
app/models/task.rb【編集】
class Task < ApplicationRecord
enum :status, { not_started: 0, in_progress: 1, closed: 2 }, validate: true
validates :title, presence: true, length: { minimum: 3, maximum: 20 }
validates :description, allow_blank: true, length: { minimum: 3, maximum: 500 }
validates :start_time, presence: true, comparison: { greater_than_or_equal_to: Time.current.beginning_of_day } #←ここ
end
モデルは開発が進むと肥大化しますので、可読性を失わない程度にコンパクトになると良いですね。
検証すると
todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001> task = Task.new(title: "test", start_time: Time.current.yesterday)
=>
#<Task:0x0000000105df0268
...
irb(main):002> task.valid?
=> false
irb(main):003> task.errors.full_messages
=> ["開始日は2023-11-21 00:00:00 +0900以上の値にしてください"]
同じ結果となりました。
5. タイムゾーン
また、お気付きの方もいらっしゃると思いますが、現状明らかに時刻が合っていません。
irb(main):012> task.start_time = Time.current
=> Tue, 21 Nov 2023 05:24:27.811528000 UTC +00:00
これは、Railsの タイムゾーン
の設定がデフォルトになっている為です。
UTC +00:00
が JST +09:00
となるように設定を行います。
application.rb
では、application.rb で現状の設定を確認しましょう。
config/application.rb【確認】
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module TodoApp
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.1
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w(assets tasks))
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
config.i18n.default_locale = :ja
end
end
24行目辺りに、コメントアウトされた設定があります。
# config.time_zone = "Central Time (US & Canada)"
これを日本時間に設定してしまいましょう。
config.time_zone='Tokyo'
config.active_record.default_timezone= :local
このように置き換えて設定しておきます。
config/application.rb【編集】
require_relative "boot"
require "rails/all"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module TodoApp
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 7.1
# Please, add to the `ignore` list any other `lib` subdirectories that do
# not contain `.rb` files, or that should not be reloaded or eager loaded.
# Common ones are `templates`, `generators`, or `middleware`, for example.
config.autoload_lib(ignore: %w(assets tasks))
# Configuration for the application, engines, and railties goes here.
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
config.time_zone='Tokyo'
config.active_record.default_timezone= :local
# config.eager_load_paths << Rails.root.join("extras")
config.i18n.default_locale = :ja
end
end
では、改めてコンソールで確認しましょう。
config
を書き換えていますので、一旦入り直します。
todo_app % rails c
Loading development environment (Rails 7.1.1)
irb(main):001>
start_time
に値が入ったインスタンスを作ります。
irb(main):001> task = Task.new(title: "test", start_time: Time.current)
=>
#<Task:0x0000000106429b98
...
irb(main):002> task.start_time
=> Tue, 21 Nov 2023 17:44:25.004300000 JST +09:00
JST +09:00
になりました。これでOKです。
6. 【課題】バリデーションについての復習
課題です。ここは復習として自分で調べて解決してみましょう。
【課題1】 end_time
にバリデーションを設定しましょう。
現在 Task
モデルの end_time
にはバリデーションが設定されていません。
これにバリデーションを設定してみましょう。仕様は以下の通りとします。
- 入力は必須ではない
- start_timeの時間よりも後の時間でなければならない。(同じ時間は含まない)
この条件を満たすバリデーションを書いてみましょう。
課題は以上です。
7. git
git
ここまでを保存しておきましょう。
todo_app % git add .
todo_app % git commit -m "Enumや日時のバリデーション"
GitHub
Githubの設定ができているのであれば、同名のリポジトリを作成し、pushしておきましょう。
おわりに
日付関連ですごく参考になる記事を紹介させて下さい。
本チャプターはここまでとなります。
次回は
- カラムの追加
- アソシエーション
- Viewヘルパー
を学びます。
ここまでお疲れ様でした。