insert_all!では不要なユニークインデックス検索をスキップ
Rails 8.1では、insert_all!メソッドが複合主キーを持つテーブルでも正常に動作するように改善されました。
背景
insert_all!はon_duplicate: :raiseを使用し、ON CONFLICT句を含まないプレーンなINSERT文を実行します。しかし、これまではfind_unique_index_forが無条件に呼び出されていたため、ユニークインデックスが不要な場合でもその存在がチェックされていました。
これにより、以下のようなケースで問題が発生していました:
- テーブルのスキーマでは
primary_key: [:shop_id, :id]のような複合主キーを持つ - しかしモデルでは
self.primary_key = :idと単一カラムを主キーとして指定している
このような設定では、insert_all!実行時に「:idに対するユニークインデックスが見つからない」というエラーが発生していました。
技術的な変更
#56666では、@on_duplicateの値が:raiseの場合にユニークインデックスの検索をスキップするように修正されています。
変更前:
@unique_by = find_unique_index_for(@unique_by)
変更後:
@unique_by = find_unique_index_for(@unique_by) if @on_duplicate != :raise
これにより、以下のコードが正常に動作するようになります:
class Cart < ApplicationRecord
self.primary_key = :id # モデルレベルでは単一カラムを主キーとして扱う
end
# スキーマ: primary_key: [:shop_id, :id]
Cart.insert_all! [{ id: 2, shop_id: 1, title: "My cart" }]
# => 成功(以前はArgumentErrorが発生)
設計判断
重要なのは、この変更がinsert_all!のみに適用される点です。insert_all(on_duplicate: :skip)とupsert_all(on_duplicate: :update)は、依然としてユニークインデックスを必要とします。
これは、これらのメソッドがON CONFLICT句のための競合ターゲットを生成する必要があるためです:
-
insert_all!: プレーンなINSERT → ユニークインデックス不要 -
insert_all:INSERT ... ON CONFLICT DO NOTHING→ ユニークインデックス必要 -
upsert_all:INSERT ... ON CONFLICT DO UPDATE→ ユニークインデックス必要
テストケースでも、insert_allとupsert_allがユニークインデックスを見つけられない場合に適切にエラーを発生させることを確認しています:
error = assert_raises ArgumentError do
Cart.insert_all [{ id: 2, shop_id: 1, title: "My cart" }]
end
assert_match "No unique index found for id", error.message
この設計により、各メソッドの意図に応じた適切なバリデーションが行われるようになりました。