🧘
【Rails】update_all で全件同じ値に…を回避!
ステージング環境のデータの中身をサンプルデータに一気に書き換えたい時がありました。
たとえば 、update_all
を使って全ユーザーの名前やメールアドレスを変えようとすると、次のようなコードを書きたくなるかもしれません。
User.update_all(name: "テストユーザー", email: "test@example.com")
もちろんこれでも更新はされますが、全員が同じ値になってしまうという落とし穴があります。それでもいいのですが、どうせなら全部異なる値にもしてみたい🧐
問題:全件同じ値になってしまう
update_all
は SQL の UPDATE ... SET
を直接実行するため、各レコードに対して個別の値を入れることができません。
UPDATE users SET name = 'テストユーザー';
このようなSQLが発行されるため、全てのユーザーが "テストユーザー"
になります。
解決策
では、個々のレコードに応じて動的に値を設定したい場合、どうすればよいでしょうか?
いくつかの解決策を紹介します。
CONCAT
で動的に加工
①:SQLのたとえば、メールアドレスを user_#{id}@example.com
のようにしたい場合は、update_all
で SQL関数を使って組み立てることができます。
User.update_all("email = CONCAT('user_', id, '@example.com')")
これにより、各行ごとに id
に応じた値が設定されます。
find_each
+ update_columns
②:ActiveRecordの find_each
を使って処理を行う方法です。
User.find_each do |user|
user.update_columns(
name: "ユーザー#{user.id}",
email: "user_#{user.id}@example.com"
)
end
-
update_columns
はバリデーションを通さず直接更新 - 件数が多いとパフォーマンスは落ちる可能性あり
activerecord-import
を使用
③: activerecord-import
を使えば、レコードをまとめて更新するような処理も効率よく書けます。
users = User.all.to_a
users.each do |user|
user.name = "ユーザー#{user.id}"
user.email = "user_#{user.id}@example.com"
end
User.import users, on_duplicate_key_update: [:name, :email]
- 1クエリでまとめて更新されるため高速
- 使い方などはこちら
比較
方法 | 個別の値可否 | パフォーマンス | 備考 |
---|---|---|---|
update_all (固定値) |
❌ | ◎ | 全件同じ値になる |
update_all + SQL関数 |
◎ | ◎ | SQL文に依存 |
find_each + update_columns
|
◎ | △ | 件数が多いと遅い |
activerecord-import |
◎ | ◎ | gemが必要、※DB依存 |
※ 使用しているDB(MySQL/PostgreSQLなど)によって on_duplicate_key_update の構文が異なるため、実際に使う前に確認しておく必要があります。
まとめ
一括更新は目的や件数、DBの種類によって適した方法が変わります。
大量の更新処理では、速度と柔軟性のバランスを考えて手法を選びましょう🚀
Discussion