👀
同じテーブルを複数のカラムで参照する中間テーブル
失敗パターン
中間テーブルを作るときに以下の 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
をよく見ると、 article1s
と aritcle2s
というテーブルを外部参照する制約が定義されていた。
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