🙌

ActiveRecordのupsert_allの挙動について

2022/12/14に公開

upsert_all はActiveRecordインスタンスの初期化を行わずに複数レコードの作成or更新を単一のSQLで出来るので、一括更新処理などを行いたいときは重宝するメソッドです。
ただ、使う時に INSERT ... ON DUPLICATE KEY UPDATE への理解が甘いこともありちょっとハマったところがあったのでそれをまとめたいと思います。
なお、この記事では、DatabaseでMysql5.7、 Rails 6.1.7 を使用するものとします。

upsert_allを使う時は更新したいカラム以外の値も渡す必要がある

例えば次のような employees テーブルがあったとします。

employees テーブル

id office_id name number created_at updated_at
1 1 野比のび太 1 2022-11-17 00:00:00 2022-11-17 00:00:00
2 1 剛田武 2 2022-11-18 00:00:00 2022-11-18 00:00:00
3 1 骨川スネ夫 3 2022-11-19 00:00:00 2022-11-19 00:00:00

この時、例えば既に保存されているレコードに関して姓と名の間に半角スペースが必要だったのでレコード1~3を更新したいすると次のように書きたくなります。

Employee.upsert_all(
  [
    { id: 1, name: '野比 のび太' },
    { id: 2, name: '剛田 武' },
    { id: 3, name: '骨川 スネ夫' }
  ]
)

が、この場合だと ActiveRecord::NotNullViolation: Mysql2::Error が発生してしまいます
理由はレコードが新規追加だったケースの時に問題があるSQLになってしまうからです。

新規追加だったケースの時に問題がある理由

上記のupsert_allを実行したときのSQLは次のようになります

INSERT INTO `sample_database`.`employees`(`id`,`name`)
VALUES
  (1, '野比 のび太'),
  (2, '剛田 武'),
  (3, '骨川 スネ夫')
ON DUPLICATE KEY UPDATE
  updated_at = (
    CASE
      WHEN(
        `name` <= >
        VALUES(`name`)
      ) THEN `sample_database`.`employees`.updated_at
      ELSE CURRENT_TIMESTAMP
    END
  ),
  `name` = VALUES(`name`)

INSERT ... ON DUPLICATE KEY UPDATE の挙動はMySQLのリファレンスマニュアルによると

  • INSERTの時はVALUES に指定した内容 + 指定していないカラムに関してはテーブル定義のデフォルト値で保存される
  • UPDATEの時はON DUPLICATE KEY UPDATE配下にて指定された内容で更新される

といった挙動になります。そして、処理結果が新規作成になるか更新になるかはSQLが実行される瞬間まで分からないため、SQL自体はCREATEでもUPDATEでも問題ないSQLである必要があります。

今回のEmployee.upsert_allはUPDATEの時は問題ないですが、CREATEの時にoffice_id,numberなどおそらくデフォルト値が無いであろうカラムに関しての指定がないため Mysql2::Error が発生してしまうのです。

どうしたら良いか?

Mysql2::Error を回避するには例えupsert_allを使って更新処理のみを行いたい場合でもデフォルト値がないカラムに関しては値を渡してあげる必要があります。

Employee.upsert_all(
  [
    { id: 1, office_id: 1, name: '野比 のび太', number: 1 },
    { id: 2, office_id: 1, name: '剛田 武', number: 2 },
    { id: 3, office_id: 1, name: '骨川 スネ夫', number: 3 }
  ]
)

Discussion