LoginSignup
16
10

More than 3 years have passed since last update.

【Rails】外部キー追加におけるreference型の使用、未使用の違い。外部キー制約によるエラーをコンソールで確認してみた。

Posted at

概要

アソシエーション設定時にテーブルへ外部キーを追加していく中で、
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



マイグレーションすると、

schema.rb
  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


これをマイグレーションすると、

schema.rb
  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も一緒に削除してくれますね。

user.rb
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

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10