0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails7 で TODOアプリを作ろう ⑨ (Enumや日時のバリデーション)

Last updated at Posted at 2023-11-21

はじめに

前回は

バリデーションメッセージのテンプレート化 として

  • コンソール
  • バリデーション
  • 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:00JST +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ヘルパー

を学びます。

ここまでお疲れ様でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?