【Rails】外部キー制約とは?

作成したアプリのコードレビューで外部キー制約を追加するように指摘されたので調べて見ました。

結論

外部キーを保存するカラムに対して関連付けしているテーブルの主キーのみを保存するように制限し、データ不整合を防止すること。 具体的には次の制約が追加される。

  • 主キーの値のみを保存できる
  • 存在しない値を外部キーとして保存させない
  • 子テーブルの外部キーに値が保存されている親テーブルのレコードは削除できない

外部キーの概要と制約を使うことのメリット・デメリット - Qiita

外部キー制約とreferences型

Railsは外部キーを保存するカラムにはreferences型を使います。 references型のカラムには次のような特徴があります。

  • インデックスは自動で付与されindex: true が不要になる
  • カラム名が自動でxxx_id の形になる
  • foreign_key: true が使えるようになる(references型以外でforeign_key: true を追加するだけでは外部キー制約にはならない)
  • foreign_key:add_foreign_key を使って書き換え可能

Railsの外部キー制約とreference型について - Qiita

外部キー制約の追加方法

外部キー制約を追加方法は2つあります。

  1. テーブル作成時にカラムに外部キー制約を追加する
  2. カラム追加時に外部キー制約を追加する

Railsで外部キー制約のついたカラムを作る時のmigrationの書き方 - Qiita

1. テーブル作成時に外部キー制約を追加する

テーブル作成のマイグレーションファイルで作成するカラムに外部キー制約を追加できます。 具体的にはreferences型のカラムを作成し、foreign_key を指定することで外部キー制約をカラムに追加することができます。

作成するカラム名によってforeign_key: の指定が2パターンあります。

  • 外部キー制約を追加するカラム名が参照テーブル名と同じ: foreign_key: true
  • 外部キー制約を追加するカラム名が参照テーブル名と異なる: foreign_key: { to_table: } で参照するテーブル名を指定が必要

また、references型のカラム名は自動でxxx_id の形になるので_id の部分は記述しない点に注意してください。

# マイグレーションファイルの基本構文

def change
  create_table :テーブル名 do |t|
    # 外部キー制約を追加するカラム名が参照テーブル名と同じ
    t.referencess :外部キー制約を追加するカラム名, foreign_key: true

    # 外部キー制約を追加するカラム名が参照テーブル名と異なる
    t.referencess :外部キー制約を追加するカラム名, foreignj_key: { to_table: :参照先のテーブル名 }
  end
end

実装例

例) Booksテーブルに作成時にUsersテーブルを参照する外部キー制約付きのカラムを追加する。

下記コマンドでマイグレーションファイルを作成します。 rails g modelマイグレーションファイルを生成する場合は次のことに注意してください。

  • null: false は自動で付与される
  • index: true は記述されませんがインデックスは付与されている
  • to_table: の記述は自動生成されないので手動で修正が必要
# 外部キー制約付きのuser_idカラムを追加するマイグレーションファイルの作成
$ rails g model Book user:references

# 外部キー制約付きのauthor_idカラムを追加するマイグレーションファイルの作成
$ rails g model Book author:references

下記マイグレーションファイルが作成されます。必要に応じて修正を追加します。

# 外部キー制約付きのuser_idカラムを追加するマイグレーションファイル

class CreateBooks< ActiveRecord::Migration[6.1]
  def change
    create_table :books do |t|
      t.references :user, null: false, foreign_key: true
    end
  end
end
# 外部キー制約付きのauthor_idカラムを追加するマイグレーションファイル

class CreateBooks< ActiveRecord::Migration[6.1]
  def change
    create_table :books do |t|
      # to_tableは手動で追記
      t.references :author, null: false, foreign_key: { to_table: :users }
    end
  end
end

修正後、rails db:migrate します。

2. カラム追加時に外部キー制約を追加する

既存のテーブルへのカラム追加時のマイグレーションファイルで外部キー制約付きのカラムを追加できます。 add_references でカラム追加と外部キー制約の追加を行います。

この際もforeign_key の追加が必要になります。 テーブル作成時に外部キー制約を追加するのと同様にカラム名によってforeign_key の書き方は2通りあります。

  • 外部キー制約を追加するカラム名が参照テーブル名と同じ: foreign_key: true
  • 外部キー制約を追加するカラム名が参照テーブル名と異なる: foreign_key: { to_table: } で参照するテーブル名を指定が必要
# マイグレーションファイルの基本構文

def change
  # 外部キー制約を追加するカラム名が参照テーブル名と同じ
  add_references :外部キー制約を追加するテーブル名, :外部キー制約付きのカラム名, foreign_key: true

  # 外部キー制約を追加するカラム名が参照テーブル名と異なる
  add_references :外部キー制約を追加するテーブル名, :外部キー制約付きのカラム名, foreign_key: { to_table: :参照先のテーブル名 } 
end

実装例

例) Usersテーブルを参照する外部キー制約付きのカラムを既存のBooksテーブルに追加する。

下記コマンドでマイグレーションファイルを作成します。 rails g migrationマイグレーションファイルを生成する場合はrails g model と同様に次のことに注意してください。

  • null: false は自動で付与される
  • index: true は記述されませんがインデックスは付与されている
  • to_table: の記述は自動生成されないので手動で修正が必要
# 外部キー制約付きのuser_idカラムを追加するマイグレーションファイルの作成
$ rails g migration AddUserRefToBooks user:references

# 外部キー制約付きのauthor_idカラムを追加するマイグレーションファイルの作成
$ rails g migration AddAuthorRefToBooks user:references

下記マイグレーションファイルが作成されます。必要に応じて修正を追加します。

# 外部キー制約付きのuser_idカラムを追加するマイグレーションファイル

class AddUserRefToBooks < ActiveRecord::Migration[6.1]
  def change
    add_reference :books, :user, null: false, foreign_key: true
  end
end
# 外部キー制約付きのauthor_idカラムを追加するマイグレーションファイル

class AddAuthorRefToBooks < ActiveRecord::Migration[6.1]
  def change
    # カラム名の修正、to_tableを手動で追記
    add_references :books, :author, foreign_key: { to_table: :users }
  end
end

修正後、rails db:migrate します。

親テーブルに紐づく子テーブルのレコードを削除するには?

外部キー制約を追加する子テーブルの外部キーに値が保存されている親テーブルのレコードは削除できないようになっています(参照整合性)。 これは保存されている子テーブルのレコードが親テーブルのレコードを削除することで外部キーで参照できなくなるのを防止するためです。

この制限を無効化し、レコードを削除するにはdependent: destroy を使います。 dependent: destroy を指定することで関連付けられたレコードも全て削除されます。

Active Record マイグレーション - Railsガイド

Active Record の関連付け - Railsガイド

例) 親テーブルUserに関連付いた子テーブルBookのレコードも削除する

# 親テーブル
class User < ApplicationRecord
  # 関連付けたレコードも削除する
  has_many :books, dependent: destroy
end

# 子テーブル
class Book < ApplicationRecord
  belongs_to :user
end

感想

外部キー制約についての曖昧さが少し解消したので良かった。 マイグレーション周りは苦手なのでガンガン触って理解していこう。

参照

外部キーの概要と制約を使うことのメリット・デメリット - Qiita

Railsの外部キー制約とreference型について - Qiita

Railsで外部キー制約のついたカラムを作る時のmigrationの書き方 - Qiita

Active Record マイグレーション - Railsガイド

Active Record の関連付け - Railsガイド