Composite Primary Key の全コンポーネントを `content_columns` から除外
Composite Primary Key (CPK) を持つモデルでも、content_columns が主キーの全要素を除外できるように修正しました。これにより、ドキュメントに記載された「プライマリ ID は除外される」挙動と実装が一致します。
背景
content_columns はプライマリキーや継承カラムなどを除外して返すメソッドですが、現在の実装は c.name == primary_key の比較に依存しています。primary_key が配列になる CPK では文字列との比較が常に偽 となり、主キー要素が除外されません。
実際に Cpk::Book のように primary_key が ["author_id", "id"] であるモデルを呼び出すと、"id" が除外されずに結果に残ります。期待される ["title", "revision"] ではなく、["id", "title", "revision"] が返ってきている点が問題です。
技術的な変更
content_columns の除外ロジックを c.name == primary_key から Array(primary_key).include?(c.name) へ置き換えました。これにより、primary_key が配列でも文字列でも同一の判定が行われ、すべての主キーコンポーネントが正しく除外されます。
変更前:
@content_columns ||= columns.reject do |c|
c.name == primary_key ||
c.name == inheritance_column ||
c.name.end_with?("_id", "_count")
end.freeze
変更後:
@content_columns ||= columns.reject do |c|
Array(primary_key).include?(c.name) ||
c.name == inheritance_column ||
c.name.end_with?("_id", "_count")
end.freeze
Array(primary_key) は単一キーの場合でも Array("id") となり、既存ロジックと同等の振る舞いを保持します。また、主キーが未定義 (nil) の場合は空配列になるため、以前と同様に除外が行われません。
テストも新規に追加され、Cpk::Book.content_columns.map(&:name) が [%w(title revision)] を返すことを確認しています。このテストは既存のシングルキーケースを壊さず、全ケースで成功します。
設計判断
この修正は 既存の CPK 用イディオム Array(primary_key) を再利用 する方針で実装されています。コードベース全体で同様の変換が使用されているため、一貫性が保たれ、将来的なメンテナンスコストが低減します。
さらに、メソッド呼び出しの追加だけで済むため 後方互換性に影響を与えません。primary_key が文字列であっても Array にラップされるだけなので、従来の単一キー動作はそのまま維持されます。
まとめ
content_columns が CPK の全コンポーネントを除外できるようになり、ドキュメント通りの動作が保証されました。実装は最小限の変更で済み、既存コードとの整合性とテスト網の拡充により信頼性が向上しています。