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



#概要
アソシエーション設定時にテーブルへ外部キーを追加していく中で、
reference型を使用して外部キーを追加した場合と
integer型で外部キーを追加した場合について違いを確認していきます。

#外部キー追加時のreference型について

例えば、UserモデルとBlogモデルで
Blogモデルにuser_idを外部キーとして設定する場合、 
モデル作成と同時に設定するなら、以下のように記述して外部キーを追加することができます。 

**reference型を使用する場合**

```ruby

$ rails g model Blog title:string content:text user:references 
``` 
<br>
マイグレーションファイルはこのようになります。

```ruby
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
```

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

```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
```

<br>

**reference型を使わず、カラム名をuser_idにしてinteger型で記述する場合**

```ruby
 
$ rails g model Blog title:string content:text user_id:integer 
```

```ruby

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
```
<br>

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


```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
```
<br>

どちらも外部キーとしてuser_idが追加されました。
しかし、reference型にはuser_id以外にも追加されているコードがあります。
これがこの２つの方法の違いで、reference型を使うと**外部キー制約、インデックス**が自動で追加されます。
reference型を使わない場合はどちらも付与されません。

##外部キー制約とは

外部キー制約が付与された場合、以下２点の成約が付きます。

**１，存在しない値の外部キーは登録できない（参照整合性）**
**２，親テーブル（user）の外部キーが子テーブル（blog）に登録されていると親テーブルは削除できない。**

１を簡単に言い直すと
主キーとして存在しないuser_idをblogsテーブルには登録できないようになるということです。

#コンソール上で試してみる

実際にコンソール上で試してみましょう。

user_id（１）のUserを作成します。
その後、Blogを作成しますが、外部キーにuser_id（１）とuser_id（３）（存在していないuser_id）のどちらもBlogの作成に成功しています。
外部キー制約が付与されていれば、存在していないuser_idであるuser_id(3)が含まれたBlogはエラーが発生して作成することはできません。

```terminal

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">
```

```terminal

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">
```

<br>

次に、２の制約についてですが、
blog_id（１）のBlogにはuser_id（１）が外部キーとして登録されているので、user_id（１）のユーザーを削除しようとしてもエラーが発生するということです。


１，２の制約のどちらもエラーを発生させるために、外部キー制約をマイグレーション実行後に追加してみます。

後から追加する場合は`add_foreign_key :blogs, :users`
（ add_foreign_key :対象のテーブル, :指定先のテーブル）
をロールバックしたマイグレーションファイルに追記します。



```ruby

  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（３）を外部キーとして、
Blogを作成しようとするもエラーが発生します。

```terminal

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"
```


続いて２の制約についてエラーを発生させるために試してみます。

user_id(2)に紐付いたBlogを作成します。
<br>

```terminal

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">
```
<br>

親レコードのuser_id（2）を削除してみると、エラーが発生します。

```terminal

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
```

<br>

```terminal

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">
```

<br>

親レコードと子レコードを一緒に削除できました！


インデックスについてはエラーを試す場面がないので、以下参照。
https://qiita.com/seiya1121/items/fb074d727c6f40a55f22