railsで初期データを入れる(seed-fuの使い方)


[seed-fu](https://github.com/mbleigh/seed-fu) を使うのが、初期データの入れ方としては良いみたいです。

railsにある仕組みとして ```rake db:seed``` があります。
これは、何も考えずに使うと実行するたびに同じデータが登録されてしまいます。
そこで、代わりにseed-fuを利用するわけです。

seed-fuは、すでに存在しているが変更したいレコードだけ更新したり、ファイル単位で実行できたり、簡単に書けるようなシンタックスシュガーがあったりと便利です。

# インストール

Gemfileに記述してインストールします。
(Rails 3.1, 3.2, 4.0, 4.1の場合は2.3を使う)

```Gemfile
gem 'seed-fu', '~> 2.3'
```

```bash:
$ bundle install
```

# rake task

rakeタスクを実行する前に、seedファイルを置くディレクトリを作成します。

```bash:
$ mkdir db/fixtures
$ mkdir db/fixtures/development
$ mkdir db/fixtures/production
```

```db/fixtures``` は最低限必要なディレクトリで、この下にseedファイルを置きます。
環境ごとに作成したい場合は、さらにその下にdevelopmentなどのディレクトリを作成します。
```SeedFu.fixture_paths``` で ```db/fixtures``` のパスを変更できます。
seedファイルは、好きな名前を付けることができ、そしてアルファベット順にロードされます。

次にrakeタスクの説明をします。

```rake db:seed_fu``` を使ってseedファイルからデータを投入します。
このタスクには二つのオプションがあります。
```FIXTURE_PATH``` でseedファイルのパスを変更します。
```FILTER```　で対象のファイルを指定します。複数の場合は、カンマ区切りにします。

seedファイル内で同様のことを書くときは、```SeedFu.seed(fixture_paths, filter)```を記述します。

# モデル作成

実際にデータ作成をする前にモデルを作成しときます。

```bash:
$ bundle exec rails g model line name:text
$ bundle exec rake db:migrate
$ bundle exec rails dbconsole
sqlite> .schema lines
CREATE TABLE "lines" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" text, "created_at" datetime, "updated_at" datetime);
```

# データ作成

seedファイルを作成します。

```db/fixtures/line.rb
Line.seed do |s|
  s.id = 1
  s.name = "日比谷線"
end
Line.seed do |s|
  s.id = 2
  s.name = "千代田線"
end
Line.seed do |s|
  s.id = 3
  s.name = "丸の内線"
end
```

DBにデータを入れます。

```bash:
$ ./bin/rake db:seed_fu
== Seed from db/fixtures/line.rb
 - Line {:id=>1, :name=>"日比谷線"}
 - Line {:id=>2, :name=>"千代田線"}
 - Line {:id=>3, :name=>"丸の内線"}
$ ./bin/rails dbconsole
sqlite> select * from lines;
1|日比谷線|2014-10-13 06:59:21.050282|2014-10-13 06:59:21.050282
2|千代田線|2014-10-13 06:59:21.055116|2014-10-13 06:59:21.055116
3|丸の内線|2014-10-13 06:59:21.056079|2014-10-13 06:59:21.056079
```

# データ更新

id=3のnameを変更してみます。

```db/fixtures/line.rb
Line.seed do |s|
  s.id = 1
  s.name = "日比谷線"
end
Line.seed do |s|
  s.id = 2
  s.name = "千代田線"
end
Line.seed do |s|
  s.id = 3
  s.name = "丸ノ内線"
end
```

```bash:
$ ./bin/rake db:seed_fu
== Seed from /Users/ko2ic/Sources/ruby/sample/db/fixtures/line.rb
 - Line {:id=>1, :name=>"日比谷線"}
 - Line {:id=>2, :name=>"千代田線"}
 - Line {:id=>3, :name=>"丸ノ内線"}
$ ./bin/rails dbconsole
sqlite> select * from lines;
1|日比谷線|2014-10-13 06:59:21.050282|2014-10-13 06:59:21.050282
2|千代田線|2014-10-13 06:59:21.055116|2014-10-13 06:59:21.055116
3|丸ノ内線|2014-10-13 06:59:21.056079|2014-10-13 07:03:49.738081
```

id=3だけが更新されていることがわかります。

## 主キーがidでない場合

まずは、先ほどのモデルの主キーを変更しときます。
今回は、idを無くして、line_idが識別子になるようにします。

```./db/migrate/20141012162955_create_lines.rb
class CreateLines < ActiveRecord::Migration
  def change
    create_table :lines, id: false do |t|
      t.string :line_id, null: false
      t.string :name
      t.timestamps
    end
    add_index :lines, :line_id, unique: true
  end
end
```

```./app/models/line.rb
class Line < ActiveRecord::Base
  self.primary_key = :line_id
end
```

```bash:
$ ./bin/rake db:migrate:reset
$ ./bin/rails dbconsole
sqlite> .schema lines
CREATE TABLE "lines" ("line_id" varchar(255) NOT NULL, "name" varchar(255), "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "index_lines_on_line_id" ON "lines" ("line_id");
```

これで、line_idが識別子のテーブルが出来ました。

次は、seed-fuを使っての主キーがidじゃない場合の指定の方法です。
seedの引数にシンボルを指定するだけです。これは複合キーでもできます。

```db/fixtures/line.rb
Line.seed(:line_id) do |s|
  s.line_id = 'odpt.Railway:TokyoMetro.Hibiya'
  s.name = '日比谷線'
end
Line.seed(:line_id) do |s|
  s.line_id = 'odpt.Railway:TokyoMetro.Chiyoda'
  s.name = '千代田線'
end
Line.seed(:line_id) do |s|
  s.line_id = 'odpt.Railway:TokyoMetro.Marunouchi'
  s.name = '丸ノ内線'
end
```


```bash:
$ ./bin/rake db:seed_fu
== Seed from ./db/fixtures/line.rb
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Hibiya", :name=>"日比谷線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Chiyoda", :name=>"千代田線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Marunouchi", :name=>"丸ノ内線"}
$ ./bin/rails dbconsole
sqlite> select * from lines;
odpt.Railway:TokyoMetro.Hibiya|日比谷線|2014-10-13 07:46:48.855379|2014-10-13 07:46:48.855379
odpt.Railway:TokyoMetro.Chiyoda|千代田線|2014-10-13 07:46:48.859168|2014-10-13 07:46:48.859168
odpt.Railway:TokyoMetro.Marunouchi|丸ノ内線|2014-10-13 07:46:48.860122|2014-10-13 07:46:48.860122
```

# シンタックスシュガー

以下のようにも記述できます。

```db/fixtures/line.rb
Line.seed(:line_id,
  { line_id: 'odpt.Railway:TokyoMetro.Hibiya', name: '日比谷線' },
  { line_id: 'odpt.Railway:TokyoMetro.Chiyoda', name: '千代田線' },
  { line_id: 'odpt.Railway:TokyoMetro.Marunouchi', name: '丸ノ内線' },
)
```

```bash:
$ ./bin/rake db:seed_fu

== Seed from /Users/ko2ic/Sources/ruby/sample/db/fixtures/line.rb
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Hibiya", :name=>"日比谷線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Chiyoda", :name=>"千代田線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Marunouchi", :name=>"丸ノ内線"}
```

# 更新したくない場合

一度作成した後は、更新したくない場合は、seed_onceを使います。

```db/fixtures/line.rb
Line.seed_once(:line_id,
  { line_id: 'odpt.Railway:TokyoMetro.Hibiya', name: '日比谷線' },
  { line_id: 'odpt.Railway:TokyoMetro.Chiyoda', name: '千代田線' },
  { line_id: 'odpt.Railway:TokyoMetro.Marunouchi', name: '丸ノ内線' },
)
```

# seedファイルを圧縮する

seed-fuは ```rb.gzip``` をそのまま扱えます。

```bash:
$ gzip -9 db/fixtures/line.rb
$ ls db/fixtures/
line.rb.gz
$ ./bin/rake db:seed_fu
== Seed from ./db/fixtures/line.rb.gz
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Hibiya", :name=>"日比谷線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Chiyoda", :name=>"千代田線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Marunouchi", :name=>"丸ノ内線"}
```

# 既存のDBからseedファイルを作成する

この例では、Lineテーブルにある値をline_gen.rbファイルとして生成します。
その処理をrakeタスクとして登録しています。
(rakeタスクにする必要もないです。)

まずはrakeタスクを自動生成します。

```bash:
$ bundle exec rails g task seed-fu-gen
```

seedファイルを作成するために書き換えます。

```lib/tasks/seed_fu_gen.rake
namespace :seed_fu_gen do
  desc 'generate seed-fu file for line.'
  task :line => :environment do |t|
    SeedFu::Writer.write('./db/fixtures/line_gen.rb', class_name: 'Line', constraints: [:line_id]) do |w|
      Line.all.each do |x|
        w << x.as_json(except: [:created_at, :updated_at])
      end
    end
  end
end
```

作ったタスクを実行します。

```bash:
$ ./bin/rake seed_fu_gen:line
```

中身を確認すると生成されていることがわかります。

```bash:
$ cat db/fixtures/line_gen.rb
# DO NOT MODIFY THIS FILE, it was auto-generated.
#
# Date: 2014-10-13 20:22:31 +0900
# Seeding Line
# Written with the command:
#
#   ./bin/rake seed_fu_gen:line
#
Line.seed(:line_id,
  {"line_id"=>"odpt.Railway:TokyoMetro.Hibiya", "name"=>"日比谷線"},
  {"line_id"=>"odpt.Railway:TokyoMetro.Chiyoda", "name"=>"千代田線"},
  {"line_id"=>"odpt.Railway:TokyoMetro.Marunouchi", "name"=>"丸ノ内線"}
)
# End auto-generated file.
```

# CSVファイルから投入

例えば、以下のようなCSVがあるとします。

```db/fixtures/line.csv
odpt.Railway:TokyoMetro.Ginza,銀座線
odpt.Railway:TokyoMetro.Tozai,東西線
```

seedファイルを以下のように記述します。

```db/fixtures/line_csv.rb
require 'csv'

csv = CSV.read('db/fixtures/line.csv')
csv.each do |line|
  line_id = line[0]
  name = line[1]

  Line.seed(:line_id) do |s|
    s.line_id = line_id
    s.name = name
  end
end
```

実行すると登録されます。

```bash:
$ bundle exec rake db:seed_fu FILTER=line_csv
== Seed from ./db/fixtures/line_csv.rb
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Ginza", :name=>"銀座線"}
 - Line {:line_id=>"odpt.Railway:TokyoMetro.Tozai", :name=>"東西線"}
```