検証環境

Ruby 2.7.1p83
Rails 6.0.2.2

次のテーブルのTeacher Modelを定義する。

1
2
3
4
5
  create_table "teachers", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

Model保存時のcallbackで最初に呼び出されるbefore_validationで確認できるようにする。

1
2
3
class Teacher < ApplicationRecord
  before_validation { binding.pry }
end

validationのデフォルトの挙動

コンソールで各タイミングのvalidation_contextを確認する。

1
2
3
4
5
6
7
t = Teacher.new
t.validation_context # => nil
t.save

# binding.pry
self # => #<Teacher:0x00007faa0d549e30 id: nil, name: nil, created_at: nil, updated_at: nil>
self.validation_context # => :create

#updateでは次のようになる。

1
2
3
4
5
6
t # => #<Teacher id: 5, name: "A", created_at: "2020-05-04 00:48:00", updated_at: "2020-05-04 00:57:07">
t.validation_context # => nil
t.update(name: 'A')

# binding.pry
self.validation_context # => :update

validationの:onオプション有りの挙動

次のvalidationをTeacher Modelに追加する。

1
  validates :name, presence: true, on: :strict

このvalidationを実行するには、#saveメソッドなどの:contextオプションで、:onオプションと同じ値を指定する。

1
2
3
4
5
6
7
8
9
10
t = Teacher.new
t.validation_context # => nil
t.save(context: :strict)

# binding.pry
self # => #<Teacher:0x00007faa0c352628 id: nil, name: nil, created_at: nil, updated_at: nil>
self.validation_context # => :strict
exit

t.errors.full_messages # => ["Name can't be blank"]

:onオプション無しのvalidationは全てのcontextで実行される

:onオプション無しのvalidationは、Model保存時にvalidation_contextを指定する場合であっても呼び出される。確認できるように、Teacher Modelを次のように変更する。

1
2
3
4
5
6
7
8
9
10
11
class Teacher < ApplicationRecord
  # before_validation { binding.pry }

  validate :name_is_presence
  validates :name, format: { with: /\A[a-zA-Z]+\z/, message: "英文字のみが使えます" }, on: :strict

  def name_is_presence
    binding.pry
    errors.add(:name, "can't be blank") if name.blank?
  end
end

このModelをコンソールで:contextオプション有りで保存すると次のようになる。

1
2
t = Teacher.new(name: 'A')
t.save(context: :strict)
1
2
3
4
5
6
From: model_relation_practice/app/models/teacher.rb:18 Teacher#name_is_presence:

    16: def name_is_presence
    17:   binding.pry
 => 18:   errors.add(:name, "can't be blank") if name.blank?
    19: end

#name_is_presence:onオプション無しのvalidationである。

1
2
3
4
5
6
# binding.pry
self # => #<Teacher:0x00007faa0d06fdd8 id: nil, name: "A", created_at: nil, updated_at: nil>
self.validation_context # => :strict
exit

t # => #<Teacher id: 14, name: "A", created_at: "2020-05-04 02:17:06", updated_at: "2020-05-04 02:17:06">

このようにvalidation_contextを指定しても:onオプションのないvalidationは呼び出される。

associationのvalidationの挙動

次にassociationのvalidationについて検証する。次のテーブル定義のStudent Modelを追加する。

1
2
3
4
5
6
7
8
9
  create_table "students", force: :cascade do |t|
    t.string "name"
    t.integer "teacher_id", null: false
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["teacher_id"], name: "index_students_on_teacher_id"
  end

  add_foreign_key "students", "teachers"

Student ModelはTeacher Modelへのbelongs_to associationを持っている。これもvalidation_contextを確認できるようにする。

1
2
3
4
5
class Student < ApplicationRecord
  belongs_to :teacher

  before_validation { binding.pry }
end

このStudent Modelを保存する場合、association teacherの値によって挙動が変わる。
#save!を使う場合は次のようになる。

  • association teacherが保存済みのTeacherの場合
    • 保存に成功する
  • association teachernilの場合
    • belongs_toによりassociation teacherは必須のため、ActiveRecord::RecordInvalidが発生する
  • association teacherが未保存の場合
    • association teacherがvalidな場合、association teacherと共に保存される
    • association teacherがinvalidな場合、association teacherの保存に失敗し、ActiveRecord::NotNullViolationが発生する

ActiveRecord::NotNullViolationの注意点

このActiveRecord::NotNullViolationActiveRecord::RecordInvalidを発生しない#saveを使う場合でも発生する。ActiveRecord::NotNullViolationを発生させないようにするためにはStudent Modelに次のvalidationを追加する。ただし保存済みのassociation teacherに対してもvalidationを実行されるようになる。

1
validates_associated :teacher

associationへのvalidation_contextの伝播

validation_contextを指定したModel保存時、一緒に保存されるassociationのvalidation_contextはどうなるか確認する。結論から言うとvalidation_contextは伝播しない。これはvalidates_associatedを使った場合にも同様である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
t = Teacher.new
s = Student.new(teacher: t)
s.save(context: :strict)

# binding.pry
# From: model_relation_practice/app/models/student.rb:16
self.validation_context # => :strict
exit

# binding.pry
# From: model_relation_practice/app/models/teacher.rb:18
self.validation_context # => :create

t = Teacher.last
s = Student.new(teacher: t)
s.save(context: :strict)

# binding.pry
# From: model_relation_practice/app/models/student.rb:16
self.validation_context # => :strict
exit

# binding.pry
# From: model_relation_practice/app/models/teacher.rb:18
self.validation_context # => :update

まとめ

本記事ではActiverRecordのvalidationとassociationの次の仕様について検証した。

  • :onオプション無しのvalidationは全てのvalidation_contextで実行される
  • validates_associatedを使わず、associationを同時に保存する場合、ActiveRecord::NotNullViolationが発生し得る
  • validation_contextはassociationには伝播しない

これらの仕様は個人的によく覚えていられないので時折見返したり修正する。