Hit the books!!

プログラミング学習記録

バリデーションとコールバック

今回も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

f:id:ud_ike:20201012203913p:plain

エラーメッセージが出ることがわかる。

コールバックとは

コールバックとは、モデルオブジェクトのライフサイクルの検証、作成、保存、更新、削除のイベントタイミングで特定の処理を呼び出し、実行させる機能。コールバックは、モデルの振る舞いに付随して呼び出されるため、処理内容はあくまでもそのモデルに関連する属性の処理に限定すべきである。

コールバックのメソッドは、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を指定することもできる。