概要
- 前回、activerecord-importを利用して、全モデルで使えるシンプルなインポート機能を実装する方法を紹介したので、今回は既存データのアップデートする機能の実装を紹介する。
- activerecord-importの記事では、 重複をチェックするカラムは primary or unique key が設定されているカラムに限定されているが、使いにくいので、それ以外のカラムでも使える実装をした。
- postgresql、mysqlいずれでもできるが、今回は postgresql の例のみ示す。
環境
- Ruby 2.3.1
- Rails 5.1.2
- postgresql 9.6.2 ( 9.5+ が必須)
方法
activerecord-importを導入
ApplicationRecordクラスに実装
前回同様ApplicationRecordクラスに smple_update クラスを実装する。
app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
def self.simple_update(path: '', col_sep: ',', per_num: 5000, conflict_target: :id)
# pathはファイル名
# col_sepは区切り文字列
# per_numはactiverecord-importで何件ごとにsqlを発行するかの値
# conflict_targetは重複をチェックするカラム
sources = {}.with_indifferent_access
columns = []
# csvデータを読み込む
CSV.foreach(path, headers: true, col_sep: col_sep) do |row|
key = row.field(conflict_target.to_s)
sources[key] = get_import_params(row)
columns = sources[key].keys if $. == 2
end
targets = where(conflict_target => sources.keys)
list = targets.map do |target|
target.attributes.merge(sources[target.send(conflict_target).to_s])
end
import list,
on_duplicate_key_update: {conflict_target: [:id], columns: columns},
batch_size: per_num
end
private
def self.get_import_params(row)
headers = row.headers & self.column_names # 存在するカラム名のみに限定する
headers.each_with_object({}) do |c, h|
h[c.to_sym] = row.field(c)
end
end
end
ちなみに、aはarray、rはrow, hはhash、cはcolの略のつもり。
使い方
モデル
- 例えば、idとnameとcolorカラムを持つFruitモデルを想定します。
app/models/fruit.rb
class Fruit < ApplicationRecord
end
csvデータ
- アップデートするカラムと重複を判断するカラムを含むcsvデータを用意します。
- アップデートしたいカラムには 必ずカラム名と同じ名称 をつけてください。なお、存在しないカラム名はアップデート対象から自動的に除外されます。(この例ではkansou)
- 重複を判断するカラムを元にアップデートする行を検索して、アップデートするカラムのデータでデータを更新します。
インポートするデータをdbフォルダ直下に用意します。
db/input.csv
name,color,kansou
apple,red,oisii
banana,yellow,amai
- 1行目はヘッダーで、2行目以降がデータです。
- アップデートしたいカラムは color
- 重複を判断するカラムは name
インポート
例えば、rails consoleから下記の様にコマンドをうてばnameカラムに基づいて重複するデータのみが、対応するcolorカラムのデータでアップデートされます。(kansouカラムは無視されます)
> Fruit.simple_update(path: 'db/input.csv', col_sep: ',', conflict_target: :name)
- 区切り文字をタブ区切りにしたい場合は
col_sep: "\t"
を指定してください。
その他
- mysqlで行いたい場合は、activerecord-importの記事 On Duplicate Key Update を参考にして修正してください。
- 重複をチェックするカラム(conflict_target)が primary or unique key が設定されているカラムならば、もっとコードはシンプルにできます。試してないけど、以下の感じ。(get_import_paramasは上のコードと同じです。)
def self.simple_update(path: '', col_sep: ',', per_num: 5000, conflict_target: :id)
list = []
columns = []
CSV.foreach(path, headers: true, col_sep: col_sep) do |row|
import_params = get_import_params(row)
list.push(import_params)
columns = import_params.keys if $. == 2
end
import list,
on_duplicate_key_update: {conflict_target: [conflict_target], columns: columns},
batch_size: per_num
end
以上