はじめに
Railsにおいて、データの一括登録、更新処理としてinsert_allやupsert_allなどの便利なメソッドがあるが、ぼーっと実装しているとバリデーションが適用されないことがあるため、記事としてとりまとめた。
結論
activerecord-importのimportメソッドを使って、ActiveRecordインスタンスを渡す。
upsert_all の問題点
Rails には upsert_all という便利なメソッドがありますが、これを使うと バリデーションが適用されない という問題があります。
例えば、StockPrice モデルに以下のバリデーションがあるとします。
class StockPrice < ApplicationRecord
validates :company_code, presence: true
validates :date, presence: true
validates :close_price, numericality: { greater_than_or_equal_to: 0 }
end
しかし、upsert_all を使うと、このバリデーションが無視されてしまいます。
StockPrice.upsert_all([{ company_code: 1234, date: "2025-03-16", close_price: -100 }])
→ close_price に -100 を入れてもエラーにならず、そのまま保存される
なぜなら、upsert_all は SQL を直接実行 するため、ActiveRecord の機能(バリデーションやコールバック)がすべてスキップされるため。
import を使うとバリデーションがかかる理由
activerecord-import の import メソッドを使うと、渡すデータの形式によってバリデーションが適用されるかどうかが決まる というのがポイント。
(1) ActiveRecord インスタンスを渡す場合(バリデーション適用される)
stock_prices = quotes.map do |quote|
StockPrice.new(
company_code: quote["Code"].to_i,
date: quote["Date"],
close_price: quote["Close"]
)
end
StockPrice.import(stock_prices, on_duplicate_key_update: { conflict_target: [:company_code, :date], columns: [:close_price] })
→ new を使うことで ActiveRecord の通常の処理が適用され、バリデーションも実行される
(2) ハッシュを渡す場合(バリデーション適用されない)
stock_prices = quotes.map do |quote|
{
company_code: quote["Code"].to_i,
date: quote["Date"],
close_price: quote["Close"]
}
end
StockPrice.import(stock_prices, on_duplicate_key_update: { conflict_target: [:company_code, :date], columns: [:close_price] })
→ この方法だと validates が効かない
activerecord-import の仕組み
ActiveRecord のオブジェクト (StockPrice.new) を渡すと
-
importは通常のsaveのように ActiveRecord の機能を通してデータを処理する。 - バリデーション (
validates) が実行される。 - コールバック (
before_save,after_save) も実行される。
ハッシュを渡すと
-
importは SQL を直接実行する形になる。 - バリデーションがスキップされる。
- コールバック (
before_save,after_save) も実行されない。 -
upsert_allに近い動作になり、DB には高速にデータが入るが、安全性が下がる。
activerecord-import の公式ドキュメントより
activerecord-import の公式ドキュメントには、以下のように記述されています。
If you pass in model instances, ActiveRecord validations and callbacks will be executed.
If you pass in an array of hashes, validations and callbacks will be skipped.
(モデルのインスタンスを渡すと、ActiveRecord のバリデーションとコールバックが実行される。ハッシュの配列を渡すと、バリデーションとコールバックはスキップされる。)
→ インスタンス (new で作成) を渡せばバリデーションが適用される
さいごに
使うメソッドによって下記の通り挙動が異なるため、アプリ仕様にならいメソッドの使い分けが重要。
| メソッド | バリデーション | コールバック | パフォーマンス |
|---|---|---|---|
upsert_all |
❌ なし | ❌ なし | ⭕ 超高速 |
import (ハッシュ) |
❌ なし | ❌ なし | ⭕ 高速 |
import (インスタンス) |
⭕ あり | ⭕ あり | ⭕ 高速 |