検証環境
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
teacher
がnil
の場合belongs_to
によりassociationteacher
は必須のため、ActiveRecord::RecordInvalid
が発生する
- association
teacher
が未保存の場合- association
teacher
がvalidな場合、associationteacher
と共に保存される - association
teacher
がinvalidな場合、associationteacher
の保存に失敗し、ActiveRecord::NotNullViolation
が発生する
- association
ActiveRecord::NotNullViolation
の注意点
このActiveRecord::NotNullViolation
はActiveRecord::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には伝播しない
これらの仕様は個人的によく覚えていられないので時折見返したり修正する。