Railsでもまとめてupdateしてクエリ発行数を抑えたい!
Railsアプリケーションで、例えば人気投票やカウンター機能で1度に複数のrecordに跨ってincrement…といったケースで 1クエリで実行したい 場合。
既存のレコードのありきの場合はMySQLでは例えば
update votes set counter=counter + 1 where id IN (1, 2, 3)
と言った書き方で出来ますが、Railsだと記述の仕方によってはUPDATEする前にSELECTのクエリが入ったり1件ずつUPDATEしてしまうのでクエリの発行数が増大してしまいます。
また同時に更新処理をする際に、SELECT〜UPDATEが完了するまでの間ロックをしないと正しく値が更新されなくなりますし、ロックをすれば(特に複数レコード同士に跨ると) デッドロックの原因になる のでパフォーマンスを重視するなら1クエリで抑えたいですね!
でもActiveRecord-importの出番なしに
Vote.where(id: [1, 2, 3]).update_all('counter = counter + 1')
と書くことが出来ます。あ…ココまではタイトル関係ないです、すみません。
さらに既存のレコードが無かったら追加したい場合は?
ただ…そこで既存のrecordが 既に存在しているか分からない 前提になるとこれでは通用しなくなりますね。
ここで考えたのが… insert ... on duplicate key update
のMysQL構文を持ってくること。
となれば表題の通り ActiveRecord-import Gem の出番になります。
しかしドキュメントを読んでいる限りでは、 on duplicate key update counter = counter + 1
みたく、columnの値を含めた計算式を入れる方法が見当たりません。
最後の望みを託してソースコードを確認すると…
この部分でどうやらそのまま文字列の式を入れるとSQL文に組み込んでくれるように見えます!
ということで、このように書けば1クエリで実行することが出来ました!
records = ids.map do |id| { id: id, counter: 1 } end Vote.import! records, on_duplicate_key_update: 'counter=counter + 1'
ソースコードのリンクはMySQL用ですが他にPostgresSQLやSQlite用も同様の実装に見えました。
ただし仕様に載ってないということは、将来のバージョンアップでサイレントで実装が変わってしまう可能性はあるので要注意ですね。