Rails6で実装されたActiveRecord#upsert_allでバルクアップデートをしようとすると

ActiveRecord::StatementInvalid: PG::CardinalityViolation: ERROR: ON CONFLICT DO UPDATE コマンドは行に再度影響を与えることはできません HINT: 同じコマンドでの挿入候補の行が同じ制約値を持つことがないようにしてください

とエラーが出たけどCardinalityViolationの意味がわからなかったので調べてみました。

CardinalityViolation とは

まず cardinality という英語の意味がわからなかったので調べてみたところ数学用語らしい。

数学、とくに集合論において、濃度、カーディナリティ(のうど、英: cardinality)とは、有限集合における「元の個数」を一般の集合に拡張したものである。集合の濃度は基数 (cardinal number) と呼ばれる数によって表される。

なんのこっちゃわからなかったので Cardinality Violationで調べてみるとそれらしいのが出てきた。

Cardinality violations occur when a query that should return only a single row returns more than one row to an Embedded SQL™ application.

一行を返すべき時に複数行返すと起きるエラーらしい。

The PG::CardinalityViolation exception occurs when a row cannot be updated a second time in the same ON CONFLICT DO UPDATE SQL query. PostgreSQL assumes this behavior would lead the same row to updated a second time in the same SQL query, in unspecified order, non-deterministically. PostgreSQL recommends it is the developer's responsibility to prevent this situation from occurring.

このPostgresqlのエラーはON CONFLICT DO UPDATEで同じ行が2度めのUPDATEをかけられた時に起きるとのこと。つまりにupsert_allにわたすハッシュの配列ないに同一の一意条件を持つとこのエラーが起きる。

例を上げて説明すると以下のようにtitleが unique なBookモデルでupsert_allを使うとAuthor2Author3どちらも更新を試みるのでエラーになるわけです。upsert_allに渡すハッシュの配列の一意条件が重複しないように取り計らいましょう。

Book.create(title: 'Book1', author: 'Author1')
Book.upsert_all([
    {title: 'Book1', author: 'Author2'}
    {title: 'Book1', author: 'Author3'}
    ], unique_by: :title
)

参照

Bulk insert support in Rails 6 | BigBinary Blog