##バルクインサートとは
INSERT
するとき、一個一個保存していると、それに応じてsqlも発行されてしまいます。
User.each do |user|
user.name = "hoge"
user.save
end
そこで、一括でINSERT
してDBに対する負担を減らそうとするときに使うのがバルクインサート。
users = []
User.each do |user|
user.name = "hoge"
users << user
end
User.import users
これでクエリを一個で済ますことができます。
##バルクアップデート
以下のように、更新させる値を指定してアップデートすることも可能.
users = []
User.each do |user|
user.name = "hoge"
users << user
end
User.import users, on_duplicate_key_update: [:name, :age]
便利なものですが、 今回,validationに引っかかり保存できなくてハマったので共有します。
##バリデーションで引っかかっている
単純にUserモデルと、それに属するPostモデルがあるとする.
# name :string
# age :integer
User < < ActiveRecord::Base
has_many :posts
end
# title :string
# content :text
User < < ActiveRecord::Base
belongs_to :user
end
今回はUserと、それに紐づくPostの値を更新するために以下のように実装するわけだが、ここに落とし穴がある。
users = []
posts = []
User.each do |user|
user.name = "taro"
user.age = 20
users << user
post = user.posts.build(title: "テスト", content: "テストテキスト")
posts << post
end
User.import! users, on_duplicate_key_update: [:name, :age]
Post.import posts
実行
Parent.import! users, on_duplicate_key_update: [:name, :age]
ActiveRecord::RecordInvalid: バリデーションに失敗しました。 Postsの値が不正です。
userが持っていたpostモデルも同時に作っていたので、そのpostモデルのバリデーションに引っかかってしまった。
とりあえず、postが保存されればuserも保存されることを確認する。
user = users.first
post = user.posts.first
post.save
true
user.save
true
やはり、子モデル(post)が正常に保存されていない状態ではuserは保存できない。
今回のミスとして、一つのモデルに対して、二つのモデルを二重に保存させようとしていたことがわかる。
つまり、どちらか一方を正常に保存させてからでないと、どちらか一方のバリデーションに引っかかり保存できないということ。
なのでuserを保存させてからpostのみバルクインサートさせるという仕様に変えた。
posts = []
User.each do |user|
user.name = "taro"
user.age = 20
user.save
post = user.posts.build(title: "テスト", content: "テストテキスト")
posts << post
end
Post.import posts
##validateオプションを使う
特にvalidatenを外すことに抵抗がなければ、以下のようにvalidate: false
を使うことでimportに成功させることもできる。
User.import users, on_duplicate_key_update: [:name, :age], validate: false
最初からこれを使えばいいわけだが, これでは子モデル(post)にユニーク制限があった場合でも全て保存されてしまうので注意したいところ。