`create_join_table`で主キーを指定できるように
create_join_tableに:primary_keyオプションが追加され、PostgreSQLの論理レプリケーションなど主キーを必要とするユースケースでも、結合テーブルをRailsのマイグレーションAPIで扱えるようになりました。
背景
create_join_tableはこれまで、内部でcreate_tableを呼び出す際にid: falseをハードコードしており、主キーの設定手段を持ちませんでした。:primary_keyオプションを渡してもid: falseが優先されるため、完全に無視されていました。
PostgreSQLの論理レプリケーションは、対象テーブルに主キーが存在することを要件とします。この制約のもとでcreate_join_tableを使おうとしても、主キーなしの結合テーブルしか作れないため、代替手段としてcreate_tableで結合テーブルを手書きするか、可逆性の管理が煩雑なカスタムマイグレーションメソッドを定義するしかありませんでした。
本PRはこの制約を解消し、create_join_tableのAPIの範囲内で主キー付き結合テーブルを作成できるようにしています。
技術的な変更
schema_statements.rbのcreate_join_tableメソッドからid: falseのハードコードが取り除かれ、:primary_keyオプションの有無によってidの値を動的に決定するようになりました。
変更前:
def create_join_table(table_1, table_2, column_options: {}, **options)
join_table_name = find_join_table_name(table_1, table_2, options)
column_options.reverse_merge!(null: false, index: false)
t1_ref, t2_ref = [table_1, table_2].map { |t| reference_name_for_table(t) }
create_table(join_table_name, **options.merge!(id: false)) do |td|
td.references t1_ref, **column_options
td.references t2_ref, **column_options
yield td if block_given?
end
end
変更後:
def create_join_table(table_1, table_2, column_options: {}, **options)
join_table_name = find_join_table_name(table_1, table_2, options)
column_options.reverse_merge!(null: false, index: false)
options.reverse_merge!(id: options[:primary_key] ? :primary_key : false)
t1_ref, t2_ref = [table_1, table_2].map { |t| reference_name_for_table(t) }
create_table(join_table_name, **options) do |td|
td.references t1_ref, **column_options
td.references t2_ref, **column_options
yield td if block_given?
end
end
追加されたのは1行のみです。options.reverse_merge!(id: options[:primary_key] ? :primary_key : false)により、:primary_keyが指定されている場合はid: :primary_key、指定されていない場合は従来通りid: falseがデフォルトとして設定されます。reverse_merge!を使うことで、呼び出し側が明示的にid:を指定した場合はそちらが優先される設計になっています。
複合主キーを持つ結合テーブルは以下のように作成できます。
create_join_table :assemblies, :parts, primary_key: [:assembly_id, :part_id]
これはPostgreSQLでは次のDDLを生成します。
CREATE TABLE assemblies_parts (
assembly_id bigint NOT NULL,
part_id bigint NOT NULL
);
ALTER TABLE ONLY "assemblies_parts"
ADD CONSTRAINT assemblies_parts_pkey PRIMARY KEY (assembly_id, part_id);
テストではtest_create_join_table_can_set_primary_keyとtest_drop_join_table_with_primary_keyが追加されており、作成・削除の両方向での動作が確認されています。
設計判断
インターフェースの簡潔さを優先し、id:の明示を不要とする設計が採用されました。
PR内ではid: :primary_keyを明示する形も検討されています。
# 検討された代替案
create_join_table :assemblies, :parts, id: :primary_key, primary_key: [:assembly_id, :part_id]
# 採用された形
create_join_table :assemblies, :parts, primary_key: [:assembly_id, :part_id]
:primary_keyを渡した場合にid:を自動的に:primary_keyへ設定することで、結合テーブルに主キーを設定したいという意図を1つのオプションで表現できます。後方互換性については、reverse_merge!によりデフォルト値を注入する方式を採ることで、既存のcreate_join_table呼び出しへの影響を完全に排除しています。
なお、id: falseがハードコードから外れたことに伴い、create_tableの:idオプションのドキュメントも「Join tables should set it to false」から「Join tables set it to false by default」へ表現が修正されており、仕様の変化が文書レベルでも一貫して反映されています。
まとめ
変更の核心は1行のreverse_merge!であり、既存APIとの後方互換性を保ちながら論理レプリケーション要件への対応を実現しています。結合テーブルをcreate_tableで代替していたコードベースでは、create_join_tableの可逆性(drop_join_tableとの対称性)を活かしたマイグレーションへの移行が可能になります。