バリデーションとコールバック
今回もRailsのモデルの話。
参考
- 独習Ruby on Rails | 小餅 良介 |本 | 通販 | Amazon
- Active Record コールバック - Railsガイド
- Active Record バリデーション - Railsガイド
バリデーションとは
バリデーションとはフォームなどから入力されたデータが妥当かどうか判断し、適正なデータだけをデータベースに取り込むための機能。
データベースの情報はモデルを通してテーブルへ登録されるので、バリデーションは該当するモデルの役割となる。次の2つのモジュールのどちらかをincludeすることで、バリデーションのメソッドを実装できる。
- ActiveModel::Model
- ActiveModel::Validations(ActiveModel::Modelにincludeされているモジュール)
バリデーションの評価のタイミング
自動で実行されるメソッド
バリデーションは、モデルに対してデータベースのテーブルを更新するメソッドを呼び出したとき、自動的に実行される。
create/save/updateが呼び出されるとバリデーションのチェックを実行し、エラーが発生した場合にはエラーを通知する。メソッドの記述によって振る舞いに違いがあるので注意が必要。
createとcreate!のように、!があるメソッドとないメソッドが存在するが、違いは以下の通り。
- !を伴わないメソッド:エラー時に例外を起こさない
- !を伴うメソッド:エラー時に例外ActiveRecoed::RecoedInvalidが発生する
任意のタイミングで評価するメソッド
バリデーション評価を更新メソッドの呼び出し時でなく、それ以外のタイミングで実行したい場合のメソッドも用意されている。
- valid?メソッド:エラーがなければtrueを、エラーがあればfalseを返す
- invalid?メソッド:valid?メソッドの真偽逆の値を返す
バリデーションの実装方法
バリデーションを実装するには、大きくわけると標準のバリデーションヘルパーを利用する方法と、独自のバリデーションヘルパーを利用する2つの方法がある。さらに、独自のバリデーションヘルパーを作成するには、モデル内でメソッドを設定する方法とValidatorクラスを使用して作成する方法がある。
- 標準のバリデーションヘルパーを使用してvalidatesメソッドで実装する
- モデル内に独自メソッドを設定しvalidateメソッドで実装する
- 独自Validatorクラスを使用する
標準バリデーションヘルパー
バリデーション名 | 内容 |
---|---|
presence | 値が存在する(空ではない)か |
absence | 値が空であるか |
length | 最長や最短など、長さが妥当であるか |
numericality | 数値であるか、オプションを指定して数値の範囲や偶数かなどを検証できる |
format | 正規表現に一致するか |
confirmation | 2つの入力内容の値が等しいか |
inclusion | ある値が含まれているか |
exclusion | ある値が含まれていないか |
acceptance | チェックボックスにチェックがあるか |
uniqueness | 値が一意であること |
validates_associated | 関連付いているモデルに対して一括でバリデーションを行う |
図書アプリのBookモデルに次のようなバリデーションを設定する。
- titleが入力されていない場合はエラーとする
- titleが入力されていないのにdescriptionが入力されている場合はエラーとする
- descriptionの長さは最大50文字とする
# app/models/book.rb class Book < ApplicationRecord validates :title, presence: true validates :description, absence: true , unless: :title? validates :description, length: { maximum: 50 } end
エラーメッセージが出ることがわかる。
コールバックとは
コールバックとは、モデルオブジェクトのライフサイクルの検証、作成、保存、更新、削除のイベントタイミングで特定の処理を呼び出し、実行させる機能。コールバックは、モデルの振る舞いに付随して呼び出されるため、処理内容はあくまでもそのモデルに関連する属性の処理に限定すべきである。
コールバックのメソッドは、1つのモデルインスタンス内部だけで使用するためプライベートメソッドとして実装する。
コールバックの呼び出されるタイミング
次のようにBookモデルを変更して実行してみると、コールバックの優先度がわかる。
# app/models/book.rb class Book < ApplicationRecord after_validation :after_valid_message before_validation :before_valid_message after_create :after_create_message after_save :after_save_message before_create :before_create_message before_save :before_save_message around_create :around_create_message around_save :around_save_message before_update :before_update_message after_update :after_update_message around_update :around_update_message private def after_valid_message puts 'after_valid' end def before_valid_message puts 'before_valid' end def after_create_message puts 'after_create' end def after_save_message puts 'after_save' end def before_create_message puts 'before_create' end def before_save_message puts 'before_save' end def around_create_message puts 'around_create開始' yield puts 'around_create終了' end def around_save_message puts 'around_save開始' yield puts 'around_save終了' end def before_update_message puts 'before_update' end def after_update_message puts 'after_update' end def around_update_message puts 'around_update開始' yield puts 'around_update終了' end end
新規登録したときのログ出力↓
Started POST "/books" for ::1 at 2020-10-12 21:38:00 +0900 Processing by BooksController#create as HTML Parameters: {"authenticity_token"=>"1rFIgjDEuiQcX8T9gyaKB5CYvXL8prfNETTuGZ2eYhw+TwhbZN6CR0zF1GM4syWQFJD98WuO0Ji27EMvgx0IKA==", "book"=>{"title"=>"テスト", "description"=>"バリデーション"}, "commit"=>"Create Book"} before_valid after_valid before_save around_save開始 before_create around_create開始 (0.2ms) begin transaction ↳ app/models/book.rb:40:in `around_create_message' Book Create (0.8ms) INSERT INTO "books" ("title", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["title", "テスト"], ["description", "バリデーション"], ["created_at", "2020-10-12 12:38:00.521036"], ["updated_at", "2020-10-12 12:38:00.521036"]] ↳ app/models/book.rb:40:in `around_create_message' around_create終了 after_create around_save終了 after_save (1.3ms) commit transaction ↳ app/controllers/books_controller.rb:30:in `block in create' Redirected to http://localhost:3000/books/5 Completed 302 Found in 12ms (ActiveRecord: 2.3ms | Allocations: 2772)
編集したときのログ出力↓
Started PATCH "/books/5" for ::1 at 2020-10-12 21:41:36 +0900 Processing by BooksController#update as HTML Parameters: {"authenticity_token"=>"M4p62jVQBQtX/EmkY0D7h2/KUgmwPpsJx0mb4hYWgumdbR/AjdV3ao05FQ+wNQS5inA+rpwB+yobPiC8HGSJ1A==", "book"=>{"title"=>"test", "description"=>"コールバック"}, "commit"=>"Update Book", "id"=>"5"} Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]] ↳ app/controllers/books_controller.rb:67:in `set_book' before_valid after_valid before_save around_save開始 before_update around_update開始 (0.1ms) begin transaction ↳ app/models/book.rb:56:in `around_update_message' Book Update (0.5ms) UPDATE "books" SET "title" = ?, "description" = ?, "updated_at" = ? WHERE "books"."id" = ? [["title", "test"], ["description", "コールバック"], ["updated_at", "2020-10-12 12:41:36.246801"], ["id", 5]] ↳ app/models/book.rb:56:in `around_update_message' around_update終了 after_update around_save終了 after_save (1.4ms) commit transaction ↳ app/controllers/books_controller.rb:44:in `block in update' Redirected to http://localhost:3000/books/5 Completed 302 Found in 11ms (ActiveRecord: 2.2ms | Allocations: 3010) Started GET "/books/5" for ::1 at 2020-10-12 21:41:36 +0900 Processing by BooksController#show as HTML Parameters: {"id"=>"5"} Book Load (0.2ms) SELECT "books".* FROM "books" WHERE "books"."id" = ? LIMIT ? [["id", 5], ["LIMIT", 1]] ↳ app/controllers/books_controller.rb:67:in `set_book' Rendering books/show.html.erb within layouts/application Rendered books/show.html.erb within layouts/application (Duration: 0.3ms | Allocations: 84) [Webpacker] Everything's up-to-date. Nothing to do Completed 200 OK in 11ms (Views: 8.7ms | ActiveRecord: 0.2ms | Allocations: 4490)
オプションで:if、:unless、:onを指定することもできる。