割と手垢のついたテーマではありますが、Railsを使ったCSVインポート処理を、1時間->10分に短縮したので、その時のポイントを列挙します。
前提
- CSVは1万件のレコードかつ、100程度の列数
- CSVインポートはRooを使用 https://github.com/roo-rb/roo
-
Parent
のhas_many関係にChild
がある
見直し前のコード(重要な部分のみ)
xlsx = Roo::Spreadsheet.open(file)
xlsx.sheet(0).each(
column1: '列1',
column2: '列2',
...
).with_index do |item, i|
parent = Parent.find_or_create_by(id: xxx)
child = Child.new
child.column1 = item[:column1]
master_item = MasterItem.find_by(name: item[:column2]) ...(1)
child.column2 = master_item.item
parent.children << child ...(3)
parent.save ...(2)
end
見直し(1): いちいちDBにアクセスしない
コード上の(1)ですが、DBから条件に一致するデータを取り出しています。1万件近いCSVデータのループ内なので、同数のselect文が走ることに。そのselect対象のテーブルのレコード数にもよりますが、あらかじめ配列等に格納していた方が良さげです。
見直し(2): こまめにsaveしない
重要な書類の作業中には定期的に保存した方が吉ですが、このケースではかえって逆効果です。
bulk insert を使って一気にinsertを行います。
https://github.com/zdennis/activerecord-import
見直し(3): <<
を使うとその時にsaveされる
parent.children << child
といった書き方で子データを格納すると、この瞬間にsaveが走ります。どのみちsaveされるので別にこれでもいいのですが、bulk insertで一気に保存しようとしているのに、この部分で足を引っ張ることになります。
見直し後のコード
master_items = MasterItem.all.pluck(:name, :item) ...(1)
parents = []
xlsx = Roo::Spreadsheet.open(file)
xlsx.sheet(0).each(
column1: '列1',
column2: '列2',
...
).with_index do |item, i|
parent = Parent.find_or_create_by(id: xxx)
master_item = master_items.find{|m| m[0]==item[:column2]} ...(1)
parent.children.build(
column1: item[:column1]
column2: master_item[1]
) ...(3)
parents << parent...(2)
end
Parent.import parents, recursive: true, on_duplicate_key_update: [:id] ...(2)
見直し後(1): 必要なDBのデータだけメモリ常にもつ
今回のケースで欲しかったのは、MasterItem
のname
に条件が一致するitem
なので、この2つだけ配列で格納して、検索自体はその配列上で行うことにしました。
見直し後(2): bulk insert & bulk update
saveしたいparent
を配列に格納して、Parent.import parents
とするだけで bulk insertが走ります。
ここでのポイントは recursive
とon_duplicate_key_update
です。
has_many関係にあるchildrenも一緒にinsertしたい場合は、recursive: true
というオプションを渡します。
また、今回は条件によってはupdateにしたいため、on_duplicate_key_update: [:id]
でupdateにも対応します。
見直し(3): buildを使う
せっかくのbulk insertを活かすためにbuild
を使って子モデルデータを格納します。
結果
冒頭にも書いてますが、見直し前のコードでは1時間ほどかかっていた処理が50分近く短縮され、約10分で完了しました。
バリデーションやロールバックなどの整備をすると速度に変動が出てくるかもしれませんが、十分問題ないレベルかなと思います。