suin.io

Rails:外部キー制約をマイグレーションで表現する方法

suin2016年8月27日

Railsのマイグレーションではreferencesを使うことで外部キー制約を表現することができます。ここではそのやり方と、index: trueオプション、foreign_key: trueオプションの違い、両者を使った場合の振る舞い、referencesでは表現できない外部キー制約について触れたいと思います。

次のようなCompanyとProjectの一対多の関係性を例に考えます。

CompanyとProjectの一対多の関係性

まず、モデルとマイグレーションファイルを生成します:

$ rails generate model Company
      invoke  active_record
      create    db/migrate/20160827081918_create_companies.rb
      create    /models/company.rb
      invoke    test_unit
      create      test/models/company_test.rb
      create      test/fixtures/companies.yml

$ rails generate model Project
      invoke  active_record
      create    db/migrate/20160827081935_create_projects.rb
      create    /models/project.rb
      invoke    test_unit
      create      test/models/project_test.rb
      create      test/fixtures/projects.yml

referencesだけでは外部キー制約にならない

次に、Projectsテーブルのマイグレーションファイルにt.references :companyを追加し、Companyに外部キー制約を貼ってみます。

class CreateProjects < ActiveRecord::Migration
  def change
    create_table :projects do |t|
      t.references :company
      t.timestamps null: false
    end
  end
end

このマイグレーションを実行すると、次のようにprojectsテーブルが生成されます。CREATE文を読んでみるとわかりますが、外部キー制約が貼られていません。referencesはデフォルトでは外部キー制約にならないようです。

CREATE TABLE `projects` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `company_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

index: trueでも外部キー制約にならない

次に、index: trueオプションを付けてマイグレーションを実行してみます。

class CreateProjects < ActiveRecord::Migration
  def change
    create_table :projects do |t|
      t.references :company, index: true
      t.timestamps null: false
    end
  end
end

実行結果がこれです。company_idカラムにインデックスはつきましたが、やはり外部キー制約にはなっていません。

CREATE TABLE `projects` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `company_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_projects_on_company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

foreign_key: trueで外部キー制約になる

今度は、foreign_key: trueを付けて実行してみます。

class CreateProjects < ActiveRecord::Migration
  def change
    create_table :projects do |t|
      t.references :company, foreign_key: true
      t.timestamps null: false
    end
  end
end

すると、company_idカラムにインデックスと外部キー制約がつきました。

CREATE TABLE `projects` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `company_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_rails_44a549d7b3` (`company_id`),
  CONSTRAINT `fk_rails_44a549d7b3` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

index: trueforeign_key: trueを組み合わせるとどうなるか

最後に、index: trueforeign_key: trueを組み合わせた場合どうなるか見ておこうと思います。

class CreateProjects < ActiveRecord::Migration
  def change
    create_table :projects do |t|
      t.references :company, index: true, foreign_key: true
      t.timestamps null: false
    end
  end
end

これによって作られるテーブルは次のようになります。foreign_key: trueだけのときは、company_idカラムのインデックス名がfk_rails_44a549d7b3というfk始まりでしたが、今回はindex_projects_on_company_idになっています。それ以外の違いはありませんね。

CREATE TABLE `projects` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `company_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_projects_on_company_id` (`company_id`),
  CONSTRAINT `fk_rails_44a549d7b3` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

同じモデルに複数外部キー制約をつける場合

次のような関連性を考えてみます。Messageには送信者と受信者がいて、両方ともUserを参照するような関連性です。

MessageテーブルとUserテーブル

まずはモデルを生成します:

$ rails generate model User
      invoke  active_record
      create    db/migrate/20160827084222_create_users.rb
      create    /models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

$ rails generate model Message
      invoke  active_record
      create    db/migrate/20160827084233_create_messages.rb
      create    /models/message.rb
      invoke    test_unit
      create      test/models/message_test.rb
      create      test/fixtures/messages.yml

次に、referencesを使ってMessagesテーブルからUsersテーブルに外部キー制約をつけてみます。foreign_keyの引数はドキュメントではbool値でしたが、もしやカラム名を指定できるのではと思ったので、試しにカラム名にしてみます。

class CreateMessages < ActiveRecord::Migration
  def change
    create_table :messages do |t|
      t.references :user, foreign_key: 'sender_id'
      t.references :user, foreign_key: 'recipient_id'
      t.timestamps null: false
    end
  end
end

マイグレーションを実行します。すると、エラーになり、マイグレーションが失敗しました。どうやら、foreign_keyの引数はbool値のみで、user_idカラムが2回作られる格好となってしまい、エラーになったようです。

$ rake db:migrate
Mysql2::Error: Can't write; duplicate key in table '#sql-a_10': ALTER TABLE `messages` ADD CONSTRAINT `fk_rails_273a25a7a6`

こうしたケースでは、referencesforeign_keyで外部キー制約を作ることができないので、add_foreign_keyを使います。次のようにマイグレーションファイルを書き換えると期待した通りの振る舞いになります。

class CreateMessages < ActiveRecord::Migration
  def change
    create_table :messages do |t|
      t.references :sender
      t.references :recipient
      t.timestamps null: false
    end
    add_foreign_key :messages, :users, column: :sender_id
    add_foreign_key :messages, :users, column: :recipient_id
  end
end

作られるテーブルは次のようになります。

CREATE TABLE `messages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `sender_id` int(11) DEFAULT NULL,
  `recipient_id` int(11) DEFAULT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_rails_b8f26a382d` (`sender_id`),
  KEY `fk_rails_12e9de2e48` (`recipient_id`),
  CONSTRAINT `fk_rails_12e9de2e48` FOREIGN KEY (`recipient_id`) REFERENCES `users` (`id`),
  CONSTRAINT `fk_rails_b8f26a382d` FOREIGN KEY (`sender_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

まとめ

Railsのマイグレーションで外部キー制約を表現するためには、

  • 基本はt.references :<参照先>, foreign_key: trueを使う
  • これで解決できない場合は、add_foreign_key :<参照元>, :<参照先>, column: :<カラム名>で対応する

ということになります。

RELATED POSTS