Railsの勉強をしていてハマったのでメモ。
あるテーブルのカラムについて、マイグレーションファイル自体に「null: false」を記入してNotNull制約をする場合と、モデル自体に「validates :カラム名, presence: true」としてnull不許可をする場合、挙動に違いがあります。
「どちらも値にnilが入らないんでしょ? 同じじゃん。」と思いがちですが、たとえば拒否できるデータ構造が異なっており、前者では「空文字("")」を拒否することが出来ません。
以下でその挙動について詳しくみていきます。
前者の場合(testsテーブルのtitleカラムにNotNull制約を付与)
class CreateTests < ActiveRecord::Migration[5.2]
def change
create_table :tests do |t|
t.string :title, null: false
t.timestamps
end
end
end
このマイグレーションファイルをbin/rails db:migrateしたあと、Railsコンソールにてnilを保存しようとすると、以下のように弾かれます。
irb(main):001:0> Test.new(title: nil).save
(中略)
ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "title" violates not-null constraint)
これは想像通りの挙動ですね。
ただし、以下のように「空文字("")」でsaveしたとき、保存できてしまいます。
irb(main):001:0> Test.new(title: "").save
(0.3ms) BEGIN
Test Create (0.6ms) INSERT INTO "tests" ("title", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" \
[["title", ""], ["created_at", "2019-02-05 10:02:31.263051"], ["updated_at", "2019-02-05 10:02:31.263051"]]
(0.8ms) COMMIT
=> true
NotNull制約したのに!
ここがポイントで、バリデーションでnull不許可した場合は、空文字も拒否することができます。
class Test < ApplicationRecord
validates :title, presence: true
end
irb(main):001:0> test = Test.new(title: "")
irb(main):002:0> test.save
(0.2ms) BEGIN
(0.2ms) ROLLBACK
=> false
これで安心ですね。
また、バリデージョンに基づいて保存に失敗した場合、このtestインスタンスにはバリデーションに基づいたエラー情報が入力されます。
irb(main):003:0> test.errors
=> #<ActiveModel::Errors:0x00007f9734965cf8 \
@base=#<Test id: nil, title: "", created_at: nil, updated_at: nil>, \
@messages={:title=>["を入力してください"], @details={:title=>[{:error=>:blank}]}>
@messages変数と、@details変数に情報が入ってますね。
(rails-i18nにより日本語化済み)
これは以下のようにして配列の形で取り出したり出来ます。
irb(main):004:0> test.errors.full_messages
=> ["titleを入力してください"]
バリデーションでのエラーの方が特殊な挙動をしますが、情報を取り出せるので便利です。
まとめ
・テーブルのカラムに定義するNotNull制約はnilは拒否するが、空文字は拒否しない。
・モデルに定義するバリデージョンでのnull不許可(presence: true)は、nilも空文字も拒否する。そして、拒否したときに(saveに失敗したときに)インスタンスの@messages変数と@details変数の中に定義したバリデーションに基づいた拒否理由を返す。
・nullを許可したくない場合は、ひとまずテーブルもモデルもnull不許可にしておくのがよさそう。