insert_all!にunique_byオプションを追加
ActiveRecord の insert_all! メソッドに unique_by オプションが追加され、insert_all との機能的な一貫性が確保されました。これにより、重複時に例外を発生させる場合でも、特定のユニークインデックスを指定した競合検出が可能になります。
背景
insert_all メソッドは既に unique_by オプションをサポートしており、PostgreSQL と SQLite において特定のユニークインデックスを競合ターゲットとして指定できました。しかし、insert_all! では同じオプションが公開されていませんでした。#45317 で最初に指摘されたこのインターフェースの不整合が、#56454 で解消されています。
内部実装である ActiveRecord::InsertAll クラスは既に unique_by パラメータをサポートしていたため、公開インターフェースが実装の能力に追いついていない状態でした。この変更により、バング(!)の有無に関わらず同じオプションセットで呼び出せるようになります。
技術的な変更
activerecord/lib/active_record/relation.rb の insert_all! メソッドのシグネチャが拡張されました。
変更前:
def insert_all!(attributes, returning: nil, record_timestamps: nil)
InsertAll.execute(self, attributes, on_duplicate: :raise, returning: returning, record_timestamps: record_timestamps)
end
変更後:
def insert_all!(attributes, returning: nil, unique_by: nil, record_timestamps: nil)
InsertAll.execute(self, attributes, on_duplicate: :raise, returning: returning, unique_by: unique_by, record_timestamps: record_timestamps)
end
unique_by パラメータがメソッドシグネチャに追加され、InsertAll.execute への呼び出しに渡されるようになりました。これにより、以下のような使用が可能になります:
Book.insert_all!(
[{ name: "UniqueBy", author_id: 1, isbn: "unique-isbn" }],
unique_by: :isbn
)
テストケースでは、部分インデックスを持つ books.isbn に対する動作が検証されています。このインデックスは published_on IS NOT NULL の条件付きで有効になるため、unique_by: :isbn を指定した場合の競合検出が正しく機能することが確認されています。
設計判断
既存の insert_all と同じオプション体系 を採用する判断がなされました。
unique_by オプションは PostgreSQL と SQLite でのみ機能し、カラム名、カラム名の配列、またはインデックス名を指定できます。ドキュメントには以下の例が追加されています:
unique_by: :isbn
unique_by: %i[ author_id name ]
unique_by: :index_books_on_isbn
on_duplicate: :raise の動作はそのまま維持され、unique_by で指定されたインデックスに対する重複が検出された場合に ActiveRecord::RecordNotUnique 例外が発生します。insert_all では on_duplicate: :skip や on_duplicate: :update との組み合わせで使用されていたオプションが、例外発生時にも同じインターフェースで利用できるようになりました。
まとめ
本PRは、insert_all! のインターフェースを insert_all と整合させる変更です。内部実装が既にサポートしていた機能を公開APIとして露出させることで、バングメソッドの有無に関わらず一貫したオプション指定が可能になりました。部分インデックスを持つテーブルにおいて、特定のユニークインデックスを明示的に指定した競合検出が insert_all! でも利用できるようになります。