投稿日:2026/9/10
更新日:2026/9/10

マイグレーションで見かける foreign_key: { to_table: :new_tags } は何をしているのか。
結論から言うと 「Rails が勝手に決めた参照先テーブルを、手で上書きしている」 だけ。
なぜ上書きが必要になるのか、書き忘れると何が起きるのかをまとめる。
t.references :tag は tag_id カラムを作り、外部キーの参照先を 参照名を複数形にした tags と推測するnew_tags なら推測が外れる。to_table: はその推測を上書きする指定Tag.table_name が new_tags でも推測は tags のままto_table: が変えるのは DB 制約の参照先だけ。カラム名は tag_id のまま、モデル側の設定は別途必要foreign_key: / inverse_of: / class_name: は別々のものを指す。順に「DB のカラム名」「相手モデルの関連名」「クラス名」ブログの記事(articles)に、タグを1つ紐づける。
ただしタグのマスタは作り直しの経緯があり、テーブル名が new_tags になっている、という状況。
erDiagram
new_tags ||--o{ articles : "1タグに複数記事"
new_tags {
uuid id PK
string name
}
articles {
uuid id PK
uuid tag_id FK "→ new_tags.id"
string title
}
create_table :articles, id: :uuid do |t|
t.string :title, null: false
t.references :tag, type: :uuid, null: false, foreign_key: { to_table: :new_tags }, comment: 'タグ'
t.timestamps
end
t.references は4つの仕事を1行でやる。オプションがどれに効くのかを分けて見ると分かりやすい。
| 記述 | 効く先 | 生成されるもの |
|---|---|---|
:tag |
カラム名 | tag_id("#{参照名}_id" 固定) |
type: :uuid |
カラム型 | uuid(既定は bigint) |
null: false |
カラム制約 | NOT NULL |
comment: 'タグ' |
カラムのコメント | COMMENT ON COLUMN |
foreign_key: { to_table: :new_tags } |
外部キー制約 | FOREIGN KEY (tag_id) REFERENCES new_tags (id) |
(省略時 index: true) |
インデックス | index_articles_on_tag_id |
実際に流れる SQL(PostgreSQL)はこうなる。
CREATE TABLE "articles" (
"id" uuid DEFAULT gen_random_uuid() NOT NULL PRIMARY KEY,
"title" character varying NOT NULL,
"tag_id" uuid NOT NULL,
CONSTRAINT "fk_rails_a1b2c3d4e5" FOREIGN KEY ("tag_id") REFERENCES "new_tags" ("id")
);
COMMENT ON COLUMN "articles"."tag_id" IS 'タグ';
CREATE INDEX "index_articles_on_tag_id" ON "articles" ("tag_id");
to_table:が影響するのはREFERENCESの後ろだけ。カラム名はtag_idのままで、new_tag_idにはならない。
t.references が参照先テーブルを決めるロジックは、実質この1行に集約される(ActiveRecord::ConnectionAdapters::ReferenceDefinition)。
def foreign_table_name
foreign_key_options.fetch(:to_table) do
Base.pluralize_table_names ? name.to_s.pluralize : name
end
end
to_table: があればそれを使うpluralize しただけのものを使うここが勘所で、参照先は Tag モデルの table_name から引かれるわけではない。
マイグレーションはモデルクラスをロードしないので、self.table_name = 'new_tags' を書いてあっても関係なく tags になる。
flowchart TD
A["t.references :tag<br/>foreign_key にハッシュを渡す"] --> B{"to_table: の指定がある?"}
B -->|ある| C["指定されたテーブルを参照<br/>new_tags"]
B -->|ない| D["参照名を pluralize<br/>tag → tags"]
D --> E{"tags テーブルは存在する?"}
E -->|存在しない| F["マイグレーション失敗<br/>relation tags does not exist"]
E -->|旧テーブルが残っている| G["成功するが<br/>間違った先を指す"]
C --> H["意図どおり"]
単なる文字列変換なので、命名がズレていれば必ず外れる。 よくズレるのはこのあたり。
| ケース | 参照名 | 推測される先 | 実テーブル |
|---|---|---|---|
| テーブルを作り直した | tag |
tags |
new_tags |
| 別名で参照する(著者=ユーザー) | author |
authors |
users |
| 自己参照(親記事) | parent |
parents |
articles |
| 単数形テーブル | setting |
settings |
setting |
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: relation "tags" does not exist
マイグレーションが止まるので、その場で気づける。
new_tags という名前がある時点で、たいてい旧 tags テーブルがまだ残っている。
この状態で to_table: を書き忘れると、制約は旧 tags を指したままマイグレーションは成功する。
-- 意図せずこうなる
CONSTRAINT "fk_rails_a1b2c3d4e5" FOREIGN KEY ("tag_id") REFERENCES "tags" ("id")
発覚するのは、新しいタグを紐づけて保存した瞬間。
PG::ForeignKeyViolation: ERROR: insert or update on table "articles" violates foreign key constraint "fk_rails_a1b2c3d4e5"
DETAIL: Key (tag_id)=(...) is not present in table "tags".
移行期間中で旧テーブルと新テーブルが両方存在するときが一番危ない。
to_table:は「念のため」ではなく、書かないと静かに壊れる指定だと思っておく。
to_table: で解決するのは DB 制約だけ。アプリ側から article.tag を引くには、モデルにも同じズレを教える必要がある。
# 方法1: モデル名は Tag のまま、テーブル名だけ差し替える
class Tag < ApplicationRecord
self.table_name = 'new_tags'
end
class Article < ApplicationRecord
belongs_to :tag # tag_id → Tag → new_tags
end
# 方法2: モデル名を NewTag にして、関連名だけ tag にする
class NewTag < ApplicationRecord
end
class Article < ApplicationRecord
belongs_to :tag, class_name: 'NewTag' # tag_id → NewTag → new_tags
end
どの設定が何を見ているかを整理すると、こうなる。
| 誰が | 何を見て | 何を決める |
|---|---|---|
マイグレーションの t.references :tag |
参照名の文字列 | カラム名 tag_id |
マイグレーションの to_table: |
明示指定 | 制約の参照先テーブル |
belongs_to :tag |
関連名 → クラス名 | 外部キーカラム tag_id と参照モデル |
class_name: |
明示指定 | 参照するモデルクラス |
self.table_name |
明示指定 | モデルが読み書きするテーブル |
マイグレーションが通ることと、
article.tagが引けることは別々に確認する。片方だけ直して安心しない。
関連: inverse_of はいつ必要か(双方向関連の自動推測)
逆方向(タグ → 記事)の関連を張ると、似た見た目の指定が3つ並ぶ。ここが一番こんがらがる場所だが、3つとも見ている場所が違う。
# NewTag 側
class NewTag < ApplicationRecord
has_many :articles, foreign_key: :tag_id, inverse_of: :tag
end
# Article 側
class Article < ApplicationRecord
belongs_to :tag, class_name: 'NewTag'
end
| 指定 | 入るもの | 今回の値 | 由来 |
|---|---|---|---|
foreign_key: |
DB のカラム名 | :tag_id |
articles.tag_id |
inverse_of: |
相手モデルの関連名 | :tag |
belongs_to :tag の :tag |
class_name: |
モデルのクラス名 | 'NewTag' |
class NewTag |
flowchart LR
FK["foreign_key: :tag_id"] --> COL["articles.tag_id<br/>(DB のカラム)"]
INV["inverse_of: :tag"] --> ASSOC["belongs_to :tag<br/>(Article の関連名)"]
CN["class_name: NewTag"] --> KLASS["class NewTag<br/>(クラス定義)"]
inverse_of: :tag の :tag の正体これは belongs_to :tag と書いた、その関連の呼び名。カラム名 tag_id と字面が似ているので紛らわしいが、まったくの別物。
証拠に、相手側の関連名だけを変えると、カラム名は tag_id のままなのに inverse_of の値だけが変わる。
# Article 側の関連名を source_tag にすると…
class Article < ApplicationRecord
belongs_to :source_tag, class_name: 'NewTag', foreign_key: :tag_id
end
# NewTag 側はこうなる。DB のカラムは何も変わっていない
class NewTag < ApplicationRecord
has_many :articles, foreign_key: :tag_id, inverse_of: :source_tag
end
foreign_key:は DB を見て決まる/inverse_of:は相手のコードを見て決まる/class_name:はクラス定義を見て決まる。
値に迷ったら「これはどこを見れば分かる情報か」を自問すると間違えない。
has_many 側にも2つとも書くのかどちらも推測が外れるから。マイグレーションで to_table: が要るのと同じ構図が、モデル側でも起きている。
foreign_key: :tag_id — has_many は外部キーカラムを「宣言しているクラス名 + _id」と推測する。NewTag なので new_tag_id を探しにいき、実カラム tag_id と一致しない。
inverse_of: :tag — 自動推測が効かない理由が2つ重なっている。
foreign_key: を指定した関連は、自動推測の対象外(can_find_inverse_of_automatically? が options[:foreign_key] の存在だけで false を返す)NewTag → :new_tag)。Article にあるのは :tag なので、そもそも見つからないinverse_of が繋がらないと、article.tag を触るたびに SELECT が飛び、親子が別インスタンスになる。詳しくは inverse_of はいつ必要か(双方向関連の自動推測)。
補足(Rails 8.1 で確認):
inverse_of:を書いてあれば、has_manyの外部キーは相手のbelongs_to :tagから導出されるのでforeign_key:は省略できる。
ただし「相手の関連名を経由してカラム名が決まる」という間接参照になるため、両方明示しておく方が読み手にやさしい。
foreign_key: に true の代わりにハッシュを渡すと、そのまま add_foreign_key のオプションになる。
| キー | 用途 |
|---|---|
to_table: |
参照先テーブルの明示 |
primary_key: |
参照先の主キーが id でないとき(例: primary_key: :code) |
name: |
制約名の明示。既定は fk_rails_<ハッシュ10桁> |
on_delete: |
親削除時の挙動(:cascade / :nullify / :restrict) |
on_update: |
親更新時の挙動 |
validate: |
false で既存行を検証せずに制約を追加(PostgreSQL) |
deferrable: |
制約チェックをトランザクション末尾まで遅らせる(PostgreSQL) |
if_not_exists: |
すでにあるならスキップ |
t.references :tag,
type: :uuid,
null: false,
foreign_key: { to_table: :new_tags, on_delete: :restrict },
comment: 'タグ'
foreign_key: { column: :hoge }は効かない。t.referencesは内部でcolumnを"#{参照名}_id"に上書きするため、ハッシュに書いたcolumn:は捨てられる。カラム名を変えたいなら参照名そのものを変える。
dependent: との住み分けは dependent オプションの使い分け(destroy・nullify・restrict) を参照。on_delete: が DB 側、dependent: が Rails 側の後始末になる。
参照名から推測できないカラムを使った外部キーは、ダンプ時に column: 付きで出力される。ここに new_tags と tag_id の組が出ていれば成功。
create_table "articles", id: :uuid, force: :cascade do |t|
t.string "title", null: false
t.uuid "tag_id", null: false, comment: "タグ"
t.index ["tag_id"], name: "index_articles_on_tag_id"
end
add_foreign_key "articles", "new_tags", column: "tag_id"
\d articles
Foreign-key constraints:
"fk_rails_a1b2c3d4e5" FOREIGN KEY (tag_id) REFERENCES new_tags(id)
type: が参照先の主キーと一致していないと落ちるtag_id を uuid にしたのに new_tags.id が bigint のままだと、制約そのものが作れない。
PG::DatatypeMismatch: ERROR: foreign key constraint "fk_rails_a1b2c3d4e5" cannot be implemented
DETAIL: Key columns "tag_id" and "id" are of incompatible types: uuid and bigint.
type: :uuid は参照先に合わせるもので、こちらの都合で決める値ではない。
add_foreign_key 単体では、ズレる方向が逆になる既存テーブルへ後から制約だけ足すときは、推測の向きがテーブル名 → カラム名に反転する。add_foreign_key :articles, :new_tags は new_tags を単数形にした new_tag_id カラムを探しにいく。
# NG: articles.new_tag_id を探して失敗する
add_foreign_key :articles, :new_tags
# OK: カラム側を明示する
add_foreign_key :articles, :new_tags, column: :tag_id
外すときも同じ。テーブル名だけで指定すると別物を見に行くので、カラムで指定するのが安全。
remove_foreign_key :articles, column: :tag_id
既定の制約名は fk_rails_ + 「元テーブル名 + カラム名」の SHA256 先頭10桁で決まる。to_table: は材料に入っていない。
tags → new_tags に貼り替えても、制約名は変わらないname: を明示するpolymorphic: true と foreign_key: は同時に指定できない参照先が1つに定まらないので当然だが、エラーは実行時に出る。
ArgumentError: Cannot add a foreign key to a polymorphic relation
index_<テーブル名>_on_<カラム名> が長くなると切り詰められる。参照名が長い場合は明示する。
t.references :tag, type: :uuid, foreign_key: { to_table: :new_tags },
index: { name: 'idx_articles_tag_id' }
| 書き方 | 制約 | 参照先 | 使いどころ |
|---|---|---|---|
t.references :tag |
付かない | — | 制約なしで運用するテーブル(履歴・ログなど) |
t.references :tag, foreign_key: true |
付く | tags(推測) |
参照名とテーブル名が一致している通常のケース |
t.references :tag, foreign_key: { to_table: :new_tags } |
付く | new_tags(明示) |
命名がズレているとき。推測が当たる場合でも明示して困ることはない |
to_table: は「参照名を複数形にする」という Rails の推測を上書きする指定self.table_name か class_name: も合わせて用意するforeign_key: は DB、inverse_of: は相手のコード、class_name: はクラス定義を見て決まる