Railsで一括でデータを更新する
はじめに
Railsでデータを大量に追加した場合には効率を考えると1つのSQLでインポートしたいときがあります。
そんな時はactiverecord-import
を使っていました。
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ではコンフリクトが発生した場合には、例外ではなく同じid
のtitle
を更新するようになります。
まとめ
個人的にはBulk InsertとかBulk UpdateはSQLでは昔からあるので、Railsの標準機能になってほしい気持ちもあります。
特に大量データの更新の場合には利用してみてください。
Let’s enjoy Rails!