はじめに
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 (インスタンス) |
⭕ あり | ⭕ あり | ⭕ 高速 |