全モデルで使えるシンプルなデータアップデート機能を実装する

# 概要
* 前回、activerecord-importを利用して、[全モデルで使えるシンプルなインポート機能を実装する](http://qiita.com/junara/items/183aaff5e5b77ec1c679)方法を紹介したので、今回は既存データのアップデートする機能の実装を紹介する。
* [activerecord-importの記事](https://github.com/zdennis/activerecord-import/wiki/On-Duplicate-Key-Update)では、 重複をチェックするカラムは __primary or unique key__ が設定されているカラムに限定されているが、使いにくいので、それ以外のカラムでも使える実装をした。
* postgresql、mysqlいずれでもできるが、今回は __postgresql__ の例のみ示す。 


# 環境
* Ruby 2.3.1
* Rails 5.1.2
* __postgresql__ 9.6.2 ( __9.5+__ が必須)

# 方法
## activerecord-importを導入
[前回参照](http://qiita.com/junara/items/183aaff5e5b77ec1c679#activerecord-import%E3%82%92%E5%B0%8E%E5%85%A5)

## ApplicationRecordクラスに実装
[前回同様](http://qiita.com/junara/items/183aaff5e5b77ec1c679#applicationrecord%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AB%E5%AE%9F%E8%A3%85)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カラムは無視されます）

```rails_console
> Fruit.simple_update(path: 'db/input.csv', col_sep: ',', conflict_target: :name)
```
* 区切り文字をタブ区切りにしたい場合は ``col_sep: "\t"`` を指定してください。

# その他
* mysqlで行いたい場合は、activerecord-importの記事 [On Duplicate Key Update](https://github.com/zdennis/activerecord-import/wiki/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

```

以上