Railsで一括でデータを更新する

はじめに

Railsでデータを大量に追加した場合には効率を考えると1つのSQLでインポートしたいときがあります。 そんな時はactiverecord-importを使っていました。

zdennis/activerecord-import: A library for bulk insertion of data into your database using ActiveRecord.

update_allを使えば特定のレコードに対して同じ値で一括更新することは可能です。では、各レコードに対してそれぞれ異なる値を一括で更新したい場合はどうすれば良いでしょうか?

activerecord-importで一括アップデート

正解はactiverecord-importを使うでした。具体的な方法は公式のWiKiにも記載されています。

On Duplicate Key Update · zdennis/activerecord-import Wiki

これははデータベースの仕組みを使って一括アップデートをサポートしているため、データバースの種類やバージョンに依存します。PostgreSQLだと9.5以上になります。

一括更新した場合のコードは次のとおりです。1つ目の引数で更新したいオブジェクトのコレクションを渡します。on_duplicate_key_update/conflict_targetでDB上で一意制約が指定されているカラムを指定します。Railsだとidとなることが多いと思います。on_duplicate_key_update/columnsで更新したいカラムを指定します。

posts = post_ids.map.with_index(1) do |post_id, i|
  post = Post.find(post_id)
  post.title = "#{i}つめの記事です"
  post
end

self.import(posts, on_duplicate_key_update: { conflict_target: %i(id), columns: %i(title)) })

上記のコードを実行すると次のようなSQLが発行されます。

INSERT INTO "posts" ("id","title","description","created_at","updated_at")
VALUES 
  (1,'1つめの記事です','2018-08-22 01:14:24.649575','2018-09-01 00:45:18.474184'),
  (2,'2つめの記事です','2018-08-22 01:14:24.649575','2018-09-01 00:45:18.474184')
ON CONFLICT (id) DO UPDATE SET "title"=EXCLUDED."title","updated_at"=EXCLUDED."updated_at"  RETURNING "id"

発行されるのはupdateではなくてinsertになります。

特に注目すべき箇所は次の箇所です。

ON CONFLICT (id) DO UPDATE SET "title"=EXCLUDED."title","updated_at"=EXCLUDED."updated_at"  RETURNING "id"

今回のコードではon_duplicate_key_update/conflict_targetで指定したidは既に存在しているため通常はinsertが行われると一意制約によってコンフリクトが発生します。

しかし、このSQLではコンフリクトが発生した場合には、例外ではなく同じidtitleを更新するようになります。

まとめ

個人的にはBulk InsertとかBulk UpdateはSQLでは昔からあるので、Railsの標準機能になってほしい気持ちもあります。

特に大量データの更新の場合には利用してみてください。

Let’s enjoy Rails!