`insert!` が `:unique_by` オプションを受け入れ、一貫性を向上
insert! が :unique_by キーワードを受け取れるようになり、insert と同様の重複回避オプションが利用可能になりました。これにより、ActiveRecord の挿入系メソッド間でインターフェースの齟齬が解消され、ドキュメント通りにオプションを指定できるようになります。
背景
insert! のシグネチャが unique_by: を欠いていたことがエラーの根本原因でした。 ActiveRecord::Persistence::ClassMethods#insert と #insert_all! は :unique_by を受け付けますが、#insert! は def insert!(attributes, returning: nil, record_timestamps: nil) という定義のみで、unique_by: を渡すと ArgumentError: unknown keyword: :unique_by が発生していました。ドキュメントは #insert_all! で利用できるオプションを参照させていたため、実装と記述が食い違っていました。この不整合は PostgreSQL や SQLite で重複回避機能を活用したいケースで顕在化していました。
技術的な変更
insert! のメソッド定義に unique_by: キーワード引数を追加し、insert_all! へ転送する実装に変更しました。 変更前後を示すと以下の通りです。
-def insert!(attributes, returning: nil, record_timestamps: nil)
- insert_all!([ attributes ], returning: returning, record_timestamps: record_timestamps)
+def insert!(attributes, returning: nil, unique_by: nil, record_timestamps: nil)
+ insert_all!([ attributes ], returning: returning, unique_by: unique_by, record_timestamps: record_timestamps)
このシグネチャ拡張はデフォルトで unique_by: nil を設定し、既存の呼び出し側に影響を与えません。さらに、テストスイートに test_insert_bang_accepts_unique_by が追加され、supports_insert_conflict_target? が true のデータベース (PostgreSQL/SQLite) で insert! が正しくオプションを受け取ることを検証しています。
+def test_insert_bang_accepts_unique_by
+ skip unless supports_insert_conflict_target?
+
+ assert_difference "Book.count", +1 do
+ Book.insert!({ name: "UniqueBy", author_id: 1, isbn: "unique-by-insert-bang" }, unique_by: :isbn)
+ end
+end
CHANGELOG には「insert! now accepts the :unique_by option, consistent with insert」というエントリが追記され、ユーザーへの告知が行われています。
設計判断
メソッドシグネチャの統一を優先し、互換性を維持する方針が採られました。 unique_by: をオプション化しデフォルトを nil としたことで、既存コードがそのまま動作し続けます。一方で、insert! と insert のインターフェースが揃うことで、開発者はどのメソッドでも同一のオプションセットを利用でき、ドキュメントと実装の乖離が解消されました。新たに導入されたテストは、プラットフォーム固有の条件下でも安全に動作することを保証しています。
まとめ
insert! に :unique_by オプションを追加し insert_all! へ転送することで、ActiveRecord の挿入系メソッド間の一貫性が確保されました。既存呼び出しへの影響はなく、重複回避機能をシンプルに利用できるようになった点が主な価値です。