【Rails】モデルに外部キーを設定する方法とよく起こるエラー内容について

2022年2月16日 9:45

はじめに

Railsでモデルに外部キーを設定する方法について説明します。

モデルに外部キーを設定する

リレーションシップ

今回は1つのブログ記事は複数のコメントを持つ1対多のリレーションシップを例に説明します。現在はArticleモデルのみ存在し、外部キーとしてArticleモデルのIDを持つCommentモデルを作成します。

モデルの作成

さっそくCommentモデルを作成していきます。ターミナルで以下のコマンドを実行します。余談ですが、string型の長さを指定したいときはname:string{20}のように長さを中括弧で囲む必要があります。

$ rails generate model Comment article:references name:string{20} content:text

上記のコマンドを実行すると、以下のようなマイグレーションファイルが作成されます。

yyyymmddhhmmss_create_comments.rb

class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      t.references :article, foreign_key: true
      t.string :name, limit: 20
      t.text :content

      t.timestamps
    end
  end
end

references型のカラムを外部キー制約付きで追加する設定が記述されています。

さらに、以下のようなCommentモデルのモデルファイルも作成されます。

comment.rb

class Comment < ApplicationRecord
  belongs_to :article
end

Articleモデルに所属することを示すbelongs_toキーワードが記述されています。なお、Articleモデルにはコマンドを実行しただけでは何も追記されないので、自分で追記する必要があります。

article.rb

class Article < ApplicationRecord
  # 以下を追記
  has_many :comments

  # 1対1の場合は以下を追記
  # has_one :comment
end

モデルの関連付けについて詳しくは以下の記事を参照してください。

マイグレーションの実行

マイグレーションファイルが用意できたら、マイグレーションを実行します。

$ rails db:migrate

上記のコマンドを実行すると、DBにcommentsテーブルが追加され、スキーマファイルが更新されます。

schema.rb

ActiveRecord::Schema.define(version: yyyy_mm_dd_hhmmss) do
  create_table "comments", force: :cascade do |t|
    t.bigint "article_id"
    t.string "name", limit: 20
    t.text "content"
    t.index ["article_id"], name: "index_comments_on_article_id"
  end

  add_foreign_key "comments", "articles"
end

マイグレーションファイルにはt.references :article, foreign_key: trueのように記述しましたが、追加されたカラム名はちゃんとarticle_idになっています。

また、マイグレーションファイルにはインデックスを追加する記述はありませんでしたが、自動でarticle_idのにインデックスが追加されています。マイグレーションファイルでreferences型で外部キーを作成するとデフォルトでインデックスが追加されるようになっています。

一意制約の追加

references型を使った外部キーに一意制約を追加して作成するには以下のように修正します。

yyyymmddhhmmss_create_comments.rb

class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      # 以下を修正
      t.references :article, foreign_key: true, index: { unique: true }
      t.string :name, limit: 20
      t.text :content

      t.timestamps
    end
  end
end

外部キーの参照先のモデルと参照元のモデルが1対1(has_onebelongs_toの関係)の場合、一意制約がないと整合性が崩れる可能性があるため注意が必要です。

外部キーの設定時によく起こるエラー内容

ここからは、references型を使って外部キーを設定するときによく起こるエラーについて説明します。

事象

上記の「マイグレーションの実行」セクションの通りマイグレートコマンドを実行すると、以下のようなエラーが発生することがあります。

$ rails db:migrate

Column `article_id` on table `comments` does not match column `id` on `articles`, which has type `integer`. To resolve this issue, change the type of the `id` column on `articles` to be :bigint. (For example `t.bigint :id`).

テーブル `comments`の列` article_id`は、タイプ `integer`の` articles`の列 `id`と一致しません。 この問題を解決するには、 `articles`の` id`列のタイプを:bigintに変更します。 (たとえば、 `t.bigint:id`)。

今回外部キーを設定しようとしているcomments.article_idのデータ型と、その参照先であるarticles.idのデータ型が違っているため、外部キーの設定が行えなかったということです。当然ながら、外部キーを設定するには参照元と参照先のデータ型が一致している必要があります。

原因

このエラーが起こるひとつの要因として、Active Recordが作成する主キー(id)のデータ型がRails 5.1から変更されたという背景があります。Rails 5.0までは主キーのデータ型としてintegerが使われていましたが、Rails 5.1からはbigintに変更されました。

つまり、Rails 5.0以前に作られたモデルの主キーを参照先とする外部キーをRails 5.1以降に作成しようとすると、参照元と参照先のデータ型の不一致が発生し、上記のようなエラーが発生することになります。

上記のエラーメッセージの場合、ArticleモデルはRails 5.0以前に作られ、Rails 5.1以降にCommentモデルを作ろうとしたと考えられます。

対処

上記のエラーコマンドには、外部キーの参照先のデータ型をbigint型に変更することが解決策として記述されていますが、既に存在するモデルの設定変更を行うよりは、これから作ろうとしているモデルの設定変更を行ったほうがいいでしょう。

yyyymmddhhmmss_create_comments.rb

class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
      # 以下を修正
      t.references :article, foreign_key: true, type: :integer
      t.string :name, limit: 20
      t.text :content

      t.timestamps
    end
  end
end

外部キーを設定するカラムにtype: integerオプションを追加します。このオプションにより、Rails 5.1以降でも主キーのデータ型にbigintではなくintegerを使うようにでき、外部キーの参照先であるarticles.idのデータ型と一致するようになります。

まとめ

Railsでモデルに外部キーを設定する方法と、よく起こるエラー内容について説明しました。本記事を参考にしていただければと思います。

関連記事

【Rails】M1チップ搭載MacでRuby on Railsの開発環境構築
# はじめに M1チップ搭載MacにRuby on Railsの開発環境を構築する手順を記載します。 - MacBook Air (M1, 2020) - macOS Monterey 12.3.1 # Homebrew ## [...]
2022年5月5日 11:56
【Rails】Rakeタスクの基本情報と作成・実行方法
# はじめに Railsには標準でRakeというGemが同梱されています。RakeはRubyで実装されたMake(UNIX系のOSで使用できるコマンド)のようなビルド作業を自動化するツールです。Ruby Make、略してRakeというわけですね。 [...]
2022年3月7日 22:12
【Rails】Capybaraのfill_inメソッドを実行すると「既存レコードの内容+指定した内容」がセットされる事象の原因と対処【RSpec】
# はじめに RSpec + Capybaraを使用して、Railsアプリの統合テストを実装しています。とあるモデルの編集画面において、入力フォームの内容を書き換えた上で送信し、レコードが更新されることを確認します。 入力フォームの内容を書 [...]
2022年1月27日 21:22
【Rails】GitHubのセキュリティアラートで発見された脆弱性を解消する方法
# はじめに GitHubにはセキュリティアラートという機能があります。セキュリティアラートはリポジトリに含まれるライブラリやパッケージの脆弱性を定期的にチェックし、脆弱性のあるライブラリやパッケージが発見されたらアラートで知らせてくれるという機 [...]
2022年1月16日 10:36
【Rails】devise-two-factorを使った2段階認証の実装方法【初学者】
# はじめに Railsアプリで2段階認証を実装するには、「rotp」というGemを使う方法の他に、「devise-two-factor」というGemを使う方法があります。「devise-two-factor」はその名の通り、IDとパスワードによ [...]
2021年12月12日 17:58