概要
以下のようなUser
を親モデルにもつPost
モデルについて
post.user_id
をデータベースに存在しないUser
のid
やnil
でセットして保存しようとするとバリデーションエラーが走ります。
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
class Post < ApplicationRecord
belongs_to :user
end
post = Post.new(user_id: 1) # idが1のUserはまだ作られていないとする
post.save
# => false
post.errors.messages
# => {:user=>["を入力してください"]}
post = Post.new
post.save
# => false
post.errors.messages
# => {:user=>["を入力してください"]}
なお、save
ではなくsave!
とした場合はActiveRecord::RecordInvalid
というエラーが発生します。
post = Post.new(user_id: 1) # idが1のUserはまだ作られていないとする
post.save!
# => /usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/validations.rb:80:in `raise_validation_error':
# バリデーションに失敗しました: Userを入力してください (ActiveRecord::RecordInvalid)
かといってoptional: true
のオプションを追加しバリデーションを実行しないようしたとしても
Post
には外部キー制約(foreign_key: true
)やnot null制約(null: false
)をつけているので
Post
を保存することができません。
class Post < ApplicationRecord
belongs_to :user, optional: true
end
post = Post.new(user_id: 1) # idが1のUserはまだ作られていないとする
post.save
# => /usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params':
# ERROR: insert or update on table "posts" violates foreign key constraint "fk_rails_5b5ddfd518"
# (ActiveRecord::InvalidForeignKey)
post.errors.messages
# => {}
post = Post.new
post.save
# => /usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': PG::NotNullViolation:
# ERROR: null value in column "user_id" of relation "posts" violates not-null constraint
# (ActiveRecord::NotNullViolation)
post.errors.messages
# => {}
post.user_id
をデータベースに存在しないUser
のid
にした場合はActiveRecord::InvalidForeignKey
というエラーが発生し、
post.user_id
をnil
にした場合はActiveRecord::NotNullViolation
というエラーが発生します。
バリデーションのエラーメッセージに何も登録されていないこともわかります。
そこでどういった条件ならPost
を保存することができるのか気になったので検証してみました。
環境
- Rails 7.0.4
- PostgreSQL14
検証方法
以下のようなnot null制約と外部キー制約を持ったNnFkPost
モデル、外部キー制約のみ持ったFkPost
モデル、not null制約のみ持ったNnPost
モデルを作成し、それぞれのモデルの関連付けをbelongs_to: user
とした場合、belongs_to: user, optional: true
とした場合、belongs_to
をつけない場合で保存できるか試してみました。
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.timestamps
end
end
end
class CreateNnFkPosts < ActiveRecord::Migration[7.0]
def change
create_table :nn_fk_posts do |t|
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end
class CreateFkPosts < ActiveRecord::Migration[7.0]
def change
create_table :fk_posts do |t|
t.references :user, foreign_key: true
t.timestamps
end
end
end
class CreateNnPosts < ActiveRecord::Migration[7.0]
def change
create_table :nn_posts do |t|
t.references :user, null: false
t.timestamps
end
end
end
create_table "users", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "fk_posts", force: :cascade do |t|
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_fk_posts_on_user_id"
end
create_table "nn_fk_posts", force: :cascade do |t|
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_nn_fk_posts_on_user_id"
end
create_table "nn_posts", force: :cascade do |t|
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_nn_posts_on_user_id"
end
add_foreign_key "fk_posts", "users"
add_foreign_key "nn_fk_posts", "users"
検証結果
エラーが発生した場合はそのエラー名を記載し保存できた場合は◯で表現しています。
関連付け | NnFkPost.create!(user_id: 1) | NnFkPost.create! |
---|---|---|
belongs_to :user |
ActiveRecord::RecordInvalid | ActiveRecord::RecordInvalid |
belongs_to :user, optional: true |
ActiveRecord::InvalidForeignKey | ActiveRecord::NotNullViolation |
関連付けなし | ActiveRecord::InvalidForeignKey | ActiveRecord::NotNullViolation |
関連付け | FkPost.create!(user_id: 1) | FkPost.create! |
---|---|---|
belongs_to :user |
ActiveRecord::RecordInvalid | ActiveRecord::RecordInvalid |
belongs_to :user, optional: true |
ActiveRecord::InvalidForeignKey | ◯ |
関連付けなし | ActiveRecord::InvalidForeignKey | ◯ |
関連付け | NnPost.create!(user_id: 1) | NnPost.create! |
---|---|---|
belongs_to :user |
ActiveRecord::RecordInvalid | ActiveRecord::RecordInvalid |
belongs_to :user, optional: true |
◯ | ActiveRecord::NotNullViolation |
関連付けなし | ◯ | ActiveRecord::NotNullViolation |
まとめ
not null制約を持たないモデルは、belongs_to
にoptional: true
のオプションをつける(かbelongs_to
を設定しない)ことで
user_id
がnil
でも作成することができるし、
外部キー制約を持たないモデルは、belongs_to
にoptional: true
のオプションをつける(かbelongs_to
を設定しない)ことで
user_id
がデータベースに存在しないUser
のid
でも作成することができるようでした!
補足
外部キー制約を持ったモデルを作成した後に、関連先の親モデルを削除しようとしても
ActiveRecord::InvalidForeignKey
のエラーが発生します。
User.create(id: 100)
# => #<User:0x000055b0b4cbc450 id: 100>
FkPost.create(user_id: 100)
# => #<FkPost:0x000055b0b4d8f1c0 id: 1, user_id: 100>
User.find(100).destroy
# => /usr/local/bundle/gems/activerecord-7.0.4/lib/active_record/connection_adapters/postgresql_adapter.rb:768:in `exec_params': PG::ForeignKeyViolation:
# ERROR: update or delete on table "users" violates foreign key constraint "fk_rails_1d06e2de76" on table "fk_posts"
# (ActiveRecord::InvalidForeignKey)
削除したい場合は親モデルにdependent: :destroy
のオプションをつけたhas_many
を設定することで
関連づけられた子モデルと一緒に削除することができます。
class User < ApplicationRecord
has_many :fk_posts, dependent: :destroy
end
User.find(100).destroy
# =>
# User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 100], ["LIMIT", 1]]
# TRANSACTION (0.3ms) BEGIN
# FkPost Load (0.4ms) SELECT "fk_posts".* FROM "fk_posts" WHERE "fk_posts"."user_id" = $1 [["user_id", 100]]
# FkPost Destroy (0.8ms) DELETE FROM "fk_posts" WHERE "fk_posts"."id" = $1 [["id", 1]]
# User Destroy (1.7ms) DELETE FROM "users" WHERE "users"."id" = $1 [["id", 100]]
# TRANSACTION (0.9ms) COMMIT
逆に親モデルを削除するときに子モデルを削除したくないってケースのときは、
has_many
にdependent: :nullify
のオプションをつけることで親モデルを削除するときに子モデルの外部キーをnil
にしてくれます。
class User < ApplicationRecord
has_many :fk_posts, dependent: :nullify
end
User.create(id: 200)
# => #<User:0x000055b0b434fc00 id: 200>
FkPost.create(user_id: 200)
# => #<FkPost:0x000055b0b499dd50 id: 1, user_id: 200>
User.find(200).destroy
# =>
# User Load (2.0ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 200], ["LIMIT", 1]]
# TRANSACTION (0.6ms) BEGIN
# FkPost Update All (0.9ms) UPDATE "fk_posts" SET "user_id" = $1 WHERE "fk_posts"."user_id" = $2 [["user_id", nil], ["user_id", 200]]
# User Destroy (1.3ms) DELETE FROM "users" WHERE "users"."id" = $1 [["id", 200]]
# TRANSACTION (1.6ms) COMMIT
FkPost.all
# => [#<FkPost:0x000055b0b4823628 id: 1, user_id: nil>]
# FkPostのuser_idはnilに更新され削除されていないことがわかる