YASD-TECH
YASD TECH
# Rails

t.references の to_table 指定(参照名とテーブル名がズレるとき)

投稿日:2026/9/10

更新日:2026/9/10

ttitleImage

t.references の foreign_key: { to_table: } — 参照名とテーブル名がズレるとき

マイグレーションで見かける foreign_key: { to_table: :new_tags } は何をしているのか。
結論から言うと 「Rails が勝手に決めた参照先テーブルを、手で上書きしている」 だけ。
なぜ上書きが必要になるのか、書き忘れると何が起きるのかをまとめる。

結論

  • t.references :tagtag_id カラムを作り、外部キーの参照先を 参照名を複数形にした tags と推測する
  • 実テーブルが new_tags なら推測が外れる。to_table:その推測を上書きする指定
  • Rails はマイグレーション中にモデルを一切見ないTag.table_namenew_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
    }
ruby
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

この1行を分解する

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)はこうなる。

sql
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 にはならない。


なぜ to_table が要るのか — Rails はモデルを見ていない

t.references が参照先テーブルを決めるロジックは、実質この1行に集約される(ActiveRecord::ConnectionAdapters::ReferenceDefinition)。

ruby
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 を指したままマイグレーションは成功する

sql
-- 意図せずこうなる
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: は「念のため」ではなく、書かないと静かに壊れる指定だと思っておく。


DB の話とモデルの話は別

to_table: で解決するのは DB 制約だけ。アプリ側から article.tag を引くには、モデルにも同じズレを教える必要がある。

ruby
# 方法1: モデル名は Tag のまま、テーブル名だけ差し替える
class Tag < ApplicationRecord
  self.table_name = 'new_tags'
end

class Article < ApplicationRecord
  belongs_to :tag   # tag_id → Tag → new_tags
end
ruby
# 方法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 はいつ必要か(双方向関連の自動推測)


foreign_key・inverse_of・class_name はそれぞれ違うものを指している

逆方向(タグ → 記事)の関連を張ると、似た見た目の指定が3つ並ぶ。ここが一番こんがらがる場所だが、3つとも見ている場所が違う

ruby
# 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 の値だけが変わる

ruby
# 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_idhas_many は外部キーカラムを「宣言しているクラス名 + _id」と推測する。NewTag なので new_tag_id を探しにいき、実カラム tag_id と一致しない。

inverse_of: :tag — 自動推測が効かない理由が2つ重なっている。

  1. foreign_key: を指定した関連は、自動推測の対象外can_find_inverse_of_automatically?options[:foreign_key] の存在だけで false を返す)
  2. 仮に対象だったとしても、自動推測は宣言側のクラス名から関連名を作る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: のハッシュに書ける主なオプション

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: すでにあるならスキップ
ruby
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 側の後始末になる。


効いているか確認する

schema.rb

参照名から推測できないカラムを使った外部キーは、ダンプ時に column: 付きで出力される。ここに new_tagstag_id の組が出ていれば成功。

ruby
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"

psql

\d articles
Foreign-key constraints:
    "fk_rails_a1b2c3d4e5" FOREIGN KEY (tag_id) REFERENCES new_tags(id)

ハマりどころ

1. type: が参照先の主キーと一致していないと落ちる

tag_iduuid にしたのに new_tags.idbigint のままだと、制約そのものが作れない。

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参照先に合わせるもので、こちらの都合で決める値ではない。

2. add_foreign_key 単体では、ズレる方向が逆になる

既存テーブルへ後から制約だけ足すときは、推測の向きがテーブル名 → カラム名に反転する。
add_foreign_key :articles, :new_tagsnew_tags を単数形にした new_tag_id カラムを探しにいく。

ruby
# NG: articles.new_tag_id を探して失敗する
add_foreign_key :articles, :new_tags

# OK: カラム側を明示する
add_foreign_key :articles, :new_tags, column: :tag_id

外すときも同じ。テーブル名だけで指定すると別物を見に行くので、カラムで指定するのが安全。

ruby
remove_foreign_key :articles, column: :tag_id

3. 制約名に参照先テーブルは含まれない

既定の制約名は fk_rails_ + 「元テーブル名 + カラム名」の SHA256 先頭10桁で決まる。to_table: は材料に入っていない。

  • 参照先を tagsnew_tags に貼り替えても、制約名は変わらない
  • 逆に、同じカラムに2本の外部キーを貼ることはできない(名前が衝突する)。意図的に複数貼るなら name: を明示する

4. polymorphic: trueforeign_key: は同時に指定できない

参照先が1つに定まらないので当然だが、エラーは実行時に出る。

ArgumentError: Cannot add a foreign key to a polymorphic relation

5. インデックス名の63文字制限(PostgreSQL)

index_<テーブル名>_on_<カラム名> が長くなると切り詰められる。参照名が長い場合は明示する。

ruby
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 の推測を上書きする指定
  • 推測はモデルを見ていない。テーブル名と参照名がズレたら必ず書く
  • 旧テーブルが残っていると、書き忘れても通ってしまい、保存時まで発覚しない
  • DB 制約とモデル設定は別。self.table_nameclass_name: も合わせて用意する
  • モデル側の3つの指定は見ている場所が違う。foreign_key: は DB、inverse_of: は相手のコード、class_name: はクラス定義を見て決まる

参考

Index

  • t.references の foreign_key: { to_table: } — 参照名とテーブル名がズレるとき
  • 結論
  • 題材
  • この1行を分解する
  • なぜ to_table が要るのか — Rails はモデルを見ていない
  • 書き忘れるとどうなるか
  • 参照先が存在しない場合 — 落ちる(まだ幸せ)
  • 旧テーブルが残っている場合 — 通ってしまう(事故)
  • DB の話とモデルの話は別
  • foreign_key・inverse_of・class_name はそれぞれ違うものを指している
  • inverse_of: :tag の :tag の正体
  • なぜ has_many 側にも2つとも書くのか
  • foreign_key: のハッシュに書ける主なオプション
  • 効いているか確認する
  • schema.rb
  • psql
  • ハマりどころ
  • 1. type: が参照先の主キーと一致していないと落ちる
  • 2. add_foreign_key 単体では、ズレる方向が逆になる
  • 3. 制約名に参照先テーブルは含まれない
  • 4. polymorphic: true と foreign_key: は同時に指定できない
  • 5. インデックス名の63文字制限(PostgreSQL)
  • 使い分け
  • まとめ
  • 参考