#概要
アソシエーション設定時にテーブルへ外部キーを追加していく中で、
reference型を使用して外部キーを追加した場合と
integer型で外部キーを追加した場合について違いを確認していきます。
#外部キー追加時のreference型について
例えば、UserモデルとBlogモデルで
Blogモデルにuser_idを外部キーとして設定する場合、
モデル作成と同時に設定するなら、以下のように記述して外部キーを追加することができます。
reference型を使用する場合
$ rails g model Blog title:string content:text user:references
マイグレーションファイルはこのようになります。
class CreateBlogs < ActiveRecord::Migration[5.2]
def change
create_table :blogs do |t|
t.string :title
t.text :content
t.references :user, foreign_key: true
t.timestamps
end
end
end
マイグレーションすると、
enable_extension "plpgsql"
create_table "blogs", force: :cascade do |t|
t.string "title"
t.text "content"
t.bigint "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_blogs_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.integer "age"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_foreign_key "blogs", "users"
end
reference型を使わず、カラム名をuser_idにしてinteger型で記述する場合
$ rails g model Blog title:string content:text user_id:integer
class CreateBlogs < ActiveRecord::Migration[5.2]
def change
create_table :blogs do |t|
t.string :title
t.text :content
t.integer :user_id
t.timestamps
end
end
end
これをマイグレーションすると、
enable_extension "plpgsql"
create_table "blogs", force: :cascade do |t|
t.string "title"
t.text "content"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.string "name"
t.integer "age"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
どちらも外部キーとしてuser_idが追加されました。
しかし、reference型にはuser_id以外にも追加されているコードがあります。
これがこの2つの方法の違いで、reference型を使うと外部キー制約、インデックスが自動で追加されます。
reference型を使わない場合はどちらも付与されません。
##外部キー制約とは
外部キー制約が付与された場合、以下2点の成約が付きます。
1,存在しない値の外部キーは登録できない(参照整合性)
2,親テーブル(user)の外部キーが子テーブル(blog)に登録されていると親テーブルは削除できない。
1を簡単に言い直すと
主キーとして存在しないuser_idをblogsテーブルには登録できないようになるということです。
#コンソール上で試してみる
実際にコンソール上で試してみましょう。
user_id(1)のUserを作成します。
その後、Blogを作成しますが、外部キーにuser_id(1)とuser_id(3)(存在していないuser_id)のどちらもBlogの作成に成功しています。
外部キー制約が付与されていれば、存在していないuser_idであるuser_id(3)が含まれたBlogはエラーが発生して作成することはできません。
irb(main):001:0> User.create(name: "test1", age: 10)
(0.1ms) BEGIN
User Create (1.2ms) INSERT INTO "users" ("name", "age", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["name", "test1"], ["age", 10], ["created_at", "2020-01-23 03:47:53.740490"], ["updated_at", "2020-01-23 03:47:53.740490"]]
(23.2ms) COMMIT
=> #<User id: 1, name: "test1", age: 10, created_at: "2020-01-23 03:47:53", updated_at: "2020-01-23 03:47:53">
irb(main):003:0> Blog.create(title: "test1", content: "test1", user_id: 1)
(0.1ms) BEGIN
Blog Create (1.2ms) INSERT INTO "blogs" ("title", "content", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "test1"], ["content", "test1"], ["user_id", 1], ["created_at", "2020-01-23 03:48:48.383505"], ["updated_at", "2020-01-23 03:48:48.383505"]]
(4.9ms) COMMIT
=> #<Blog id: 1, title: "test1", content: "test1", user_id: 1, created_at: "2020-01-23 03:48:48", updated_at: "2020-01-23 03:48:48">
irb(main):004:0> Blog.create(title: "test1", content: "test1", user_id: 3)
(0.2ms) BEGIN
Blog Create (0.5ms) INSERT INTO "blogs" ("title", "content", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "test1"], ["content", "test1"], ["user_id", 3], ["created_at", "2020-01-23 03:48:56.834930"], ["updated_at", "2020-01-23 03:48:56.834930"]]
(1.7ms) COMMIT
=> #<Blog id: 2, title: "test1", content: "test1", user_id: 3, created_at: "2020-01-23 03:48:56", updated_at: "2020-01-23 03:48:56">
次に、2の制約についてですが、
blog_id(1)のBlogにはuser_id(1)が外部キーとして登録されているので、user_id(1)のユーザーを削除しようとしてもエラーが発生するということです。
1,2の制約のどちらもエラーを発生させるために、外部キー制約をマイグレーション実行後に追加してみます。
後から追加する場合はadd_foreign_key :blogs, :users
( add_foreign_key :対象のテーブル, :指定先のテーブル)
をロールバックしたマイグレーションファイルに追記します。
def change
create_table :blogs do |t|
t.string :title
t.text :content
t.integer :user_id
t.timestamps
end
add_foreign_key :blogs, :users
end
end
rails db:rollback
して追記した後に再度rails db:migrate
を実行します。
そして、先程と同じように存在しないuser_id(3)を外部キーとして、
Blogを作成しようとするもエラーが発生します。
irb(main):001:0> Blog.create(title: "test2", content: "test2", user_id: 3)
(0.1ms) BEGIN
Blog Create (5.7ms) INSERT INTO "blogs" ("title", "content", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "test2"], ["content", "test2"], ["user_id", 3], ["created_at", "2020-01-23 04:03:15.314345"], ["updated_at", "2020-01-23 04:03:15.314345"]]
(0.2ms) ROLLBACK
Traceback (most recent call last):
1: from (irb):1
ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR: insert or update on table "blogs" violates foreign key constraint "fk_rails_40ebb3948d")
DETAIL: Key (user_id)=(3) is not present in table "users".
: INSERT INTO "blogs" ("title", "content", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"
続いて2の制約についてエラーを発生させるために試してみます。
user_id(2)に紐付いたBlogを作成します。
irb(main):005:0> Blog.create(title: "test1", content: "test1", user_id: 2)
(0.2ms) BEGIN
Blog Create (0.7ms) INSERT INTO "blogs" ("title", "content", "user_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["title", "test1"], ["content", "test1"], ["user_id", 2], ["created_at", "2020-01-23 04:08:42.845532"], ["updated_at", "2020-01-23 04:08:42.845532"]]
(4.0ms) COMMIT
=> #<Blog id: 2, title: "test1", content: "test1", user_id: 2, created_at: "2020-01-23 04:08:42", updated_at: "2020-01-23 04:08:42">
親レコードのuser_id(2)を削除してみると、エラーが発生します。
irb(main):008:0> User.find(2).destroy
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.2ms) BEGIN
User Destroy (2.5ms) DELETE FROM "users" WHERE "users"."id" = $1 [["id", 2]]
(0.1ms) ROLLBACK
Traceback (most recent call last):
1: from (irb):8
ActiveRecord::InvalidForeignKey (PG::ForeignKeyViolation: ERROR: update or delete on table "users" violates foreign key constraint "fk_rails_40ebb3948d" on table "blogs")
DETAIL: Key (id)=(2) is still referenced from table "blogs".
: DELETE FROM "users" WHERE "users"."id" = $1
この通り、外部キー制約がついた状態では子レコードを削除してからでないと親レコードを削除できなくなっています。
親レコードを削除する時にいちいち子レコードを全て削除するのは手間がかかりますが、
Userモデルにhas_many :blogs, dependent: :destroy
を記述すれば、解決されます。
この記述でUserを削除した際に紐付いている子レコードのblogも一緒に削除してくれますね。
class User < ApplicationRecord
has_many :blogs, dependent: :destroy
end
irb(main):002:0> User.find(2).destroy
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 2], ["LIMIT", 1]]
(0.1ms) BEGIN
Blog Load (0.4ms) SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" = $1 [["user_id", 2]]
Blog Destroy (0.4ms) DELETE FROM "blogs" WHERE "blogs"."id" = $1 [["id", 2]]
User Destroy (0.5ms) DELETE FROM "users" WHERE "users"."id" = $1 [["id", 2]]
(0.5ms) COMMIT
=> #<User id: 2, name: "test2", age: 20, created_at: "2020-01-23 03:48:02", updated_at: "2020-01-23 03:48:02">
親レコードと子レコードを一緒に削除できました!
インデックスについてはエラーを試す場面がないので、以下参照。
https://qiita.com/seiya1121/items/fb074d727c6f40a55f22