👀

同じテーブルを複数のカラムで参照する中間テーブル

に公開

失敗パターン

中間テーブルを作るときに以下の rails generate ではエラーが起きなかったが、その後の rails db:migrate でエラーが発生した。

shell
> rails g model Junction article:references article:references
migrate/create_junctions.rb
class CreateJunctions < ActiveRecord::Migration[7.0]
  def change
    create_table :junctions do |t|
      # :articleを2回書いていて定義が重複している
      t.references :article, null: false, foreign_key: true
      t.references :article, null: false, foreign_key: true

      t.timestamps
  end
end
shell
❯ rails db:migrate
== 20250625124905 CreateMentions: migrating ===================================
-- create_table(:junctions)
rails aborted!
StandardError: An error has occurred, this and all later migrations canceled:

you can't define an already defined column 'article_id'.
# 略

試しにカラム名に連番をつけて実行してみると今度は db:migrate でもエラーが出なかった。

shell
> rails g model Junction article1:references article2:references
migrate/create_junctions.rb
class CreateJunctions < ActiveRecord::Migration[7.0]
  def change
    create_table :junctions do |t|
      t.references :article1, null: false, foreign_key: true
      t.references :article2, null: false, foreign_key: true
      t.timestamps
    end
  end
end

しかし schema.rb をよく見ると、 article1saritcle2s というテーブルを外部参照する制約が定義されていた。
article1 という名称を複数形にしてテーブル名を記載しており、これだと存在しないテーブルとの外部参照制約になってしまう。
:references の前にはちゃんと存在するモデル名を書きましょう。

schema.rb
add_foreign_key "junctions", "article1s"
add_foreign_key "junctions", "article2s"

成功パターン

正しくは以下のようにする。
適当にモデルをつくってマイグレーションファイルを直接編集する。

migrate/create_junctions.rb
class CreateJunctions < ActiveRecord::Migration[7.0]
  def change
    create_table :junctions do |t|
      t.references :from_article, null: false
      t.references :to_article, null: false

      t.timestamps
    end
    add_foreign_key :junctions, :articles, column: :from_article_id
    add_foreign_key :junctions, :articles, column: :to_article_id
  end
end

記事の組み合わせが重複しないようにするにはDBにユニーク制約をつけて、アプリ側でもバリデーションチェックを行う。

migrate/create_junctions.rb
class CreateJunctions < ActiveRecord::Migration[7.0]
  def change
    create_table :junctions do |t|
      t.references :from_article, null: false
      t.references :to_article, null: false

      t.timestamps
    end
    add_foreign_key :junctions, :articles, column: :from_article_id
    add_foreign_key :junctions, :articles, column: :to_article_id
    add_index :junctions, [:from_article_id, :to_article_id], unique: true #追加
  end
end
model/junction.rb
class Junction < ApplicationRecord
  belongs_to :from_article, class_name: 'Article'
  belongs_to :to_article, class_name: 'Article'

  validates :from_article_id, uniqueness: { scope: :to_article_id } #追加
end

rails console で確認してみる。

rails-console
irb(main):005:0> a1 = Article.create!(id: 1)
irb(main):005:0> a2 = Article.create!(id: 2)
irb(main):005:0> Junction.create!(from_article: a1, to_article: a2)
irb(main):005:0> Junction.create!(from_article: a1, to_article: a2) #2回目でバリデーションに失敗する
`raise_validation_error': バリデーションに失敗しました: From articleはすでに存在します

バリデーションを削除するとDB側の制約に引っかかったことを確認できる。

model/junction.rb
class Junction < ApplicationRecord
  belongs_to :from_article, class_name: 'Article'
  belongs_to :to_article, class_name: 'Article'

  validates :from_article_id, uniqueness: { scope: :to_article_id } #削除
end
rails-console
irb(main):005:0> a1 = Article.create!(id: 1)
irb(main):005:0> a2 = Article.create!(id: 2)
irb(main):005:0> Junction.create!(from_article: a1, to_article: a2)
irb(main):005:0> Junction.create!(from_article: a1, to_article: a2)
UNIQUE constraint failed: mentions.from_report_id, mentions.to_report_id (SQLite3::ConstraintException)

参考記事

【Rails】1つのテーブルに複数の外部キーを設定 #Rails - Qiita
1 つのモデル(テーブル)に複数の外部キーをもたせる - 小さなエンドウ豆
railsの中間テーブルでデータの組み合わせを一意にする方法 #Ruby - Qiita
アソシエーションにおけるclass_nameの定義! #Ruby - Qiita

Discussion