RailsのDBの初期データ(rake db:seed用)をyamlで美しく管理する方法

# この記事のサマリ
RailsにおいてDBの初期データは、seed.rbを記述することで設定することが可能ですが、全てコーディングしてしまうと見辛いので、yamlで管理する方法を紹介します。
管理するyamlファイルは、開発していく中で動かしながら検討した動作実績のあるデータを、そのままファイルに出力して活用する方式としています。
yaml_dbなどの、Gemは特に利用していません。(yaml_dbだと、rake db:seed以外のrakeコマンドが必要になりそうであり、標準のrake db:seedを利用するようにしておきたかったため)

# 動作確認バージョン
Rails 4.1.1
ruby 2.1.2p95

# 経緯
railsのアプリのDBの初期データって、seed.rbに記述して`rake db:seed`ってのがセオリーだと認識しているのですが、seed.rbにコーディングすると、イマイチ初期データの視認性が悪いので、なにか視認性の高いフォーマットで管理したい...
視認性の高いフォーマットという視点で考えたときに、csvやtsvよりは、xmlとかjsonとかyamlの方が、分かりやすい...
Rubyでやるならば、とりあえずRubyと親和性の高いyamlで管理するようにしようと思ったのが発端です。(yaml_dbで管理するのも在りですが、標準で用意されている`rake db:seed`ではない、rakeタスクの実行が必要になりそう(seed.rbにyaml_dbのrakeタスクの呼び出せるならそれで代替するのはありかも？)だったので、使わない方針としています。
また、seed.rbでセットアップしたいようなマスタデータの中身って、実際には開発しながら固めていくケースも多いと思います。なので、「rails console」で、楽にyamlに出力できるようにしておきたいと考えました。

# 具体的な手順
とりあえずサンプルとして、railsアプリを作成する。

```bash
rails new initDbDataSample

cd initDbDataSample/

# 初期データ用のyamlを保存するフォルダを作成
mkdir db/seeds

# サンプル用に、「状態」を管理するマスターDBのscaffoldを作成
rails g scaffold db_status status_name:text
rake db:migrate

# なんか適当にデータを投入する。
rails c

irb(main):001:0> ['hoge','fuga','piyo'].each { |status| DbStatus.create status_name:status }
   (0.3ms)  begin transaction
  SQL (1.1ms)  INSERT INTO "db_statuses" ("created_at", "status_name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2016-02-06 16:17:12.384006"], ["status_name", "hoge"], ["updated_at", "2016-02-06 16:17:12.384006"]]
   (5.2ms)  commit transaction
   (0.2ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "db_statuses" ("created_at", "status_name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2016-02-06 16:17:12.408840"], ["status_name", "fuga"], ["updated_at", "2016-02-06 16:17:12.408840"]]
   (7.0ms)  commit transaction
   (3.2ms)  begin transaction
  SQL (0.9ms)  INSERT INTO "db_statuses" ("created_at", "status_name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2016-02-06 16:17:12.428236"], ["status_name", "piyo"], ["updated_at", "2016-02-06 16:17:12.428236"]]
   (10.9ms)  commit transaction
[
    [0] "hoge",
    [1] "fuga",
    [2] "piyo"
]
irb(main):002:0> DbStatus.all
  DbStatus Load (0.7ms)  SELECT "db_statuses".* FROM "db_statuses"
[
    [0] #<DbStatus:0x007f81c7835c20> {
                 :id => 1,
        :status_name => "hoge",
         :created_at => Sat, 06 Feb 2016 16:17:12 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 16:17:12 UTC +00:00
    },
    [1] #<DbStatus:0x007f81c78359c8> {
                 :id => 2,
        :status_name => "fuga",
         :created_at => Sat, 06 Feb 2016 16:17:12 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 16:17:12 UTC +00:00
    },
    [2] #<DbStatus:0x007f81c7835770> {
                 :id => 3,
        :status_name => "piyo",
         :created_at => Sat, 06 Feb 2016 16:17:12 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 16:17:12 UTC +00:00
    }
]
irb(main):003:0> exit

# 上記は、例としてrailsのconsoleから設定したが、開発していく中でデータを整備するのもよし、
# 例えば外部からデータファイルが提供されているなどで、それをloadするのもよし。
```

登録されているデータをyamlにするコード

```ruby
# yamlに吐き出す対象のmodelクラス名を指定して、
target_model = DbStatus
# 吐き出したい対象のリストを取得する(下記は全件取得しているが、テスト環境だけのデータが入っているような場合は適宜絞り込む)
target_records = target_model.all
write_filepath = "#{Rails.root}/db/seeds/#{target_model.to_s.tableize}.yml"
open(write_filepath,"w") do |write_file|
  write_file.puts target_records.to_yaml
end
```

上記コードを利用して、例えば以下のような感じで、yamlに出力する。
やっていることとしては、yamlに出力する対象のデータに対応するモデルにて、レコードを抽出したのち、to_yamlでyamlに変換したデータをファイルに書き込むという流れで実施しています。

実際にyamlに吐き出している手順を、以下に例示します。

```bash
# 初期化データとして利用したいレコードをファイルに出力する。
rails c 

irb(main):001:0> # yamlに吐き出す対象のmodelを指定して、
  write_file.puts target_records.to_yaml
endirb(main):002:0* target_model = DbStatus
class DbStatus < ActiveRecord::Base {
             :id => :integer,
    :status_name => :text,
     :created_at => :datetime,
     :updated_at => :datetime
}
irb(main):003:0> # 吐き出したい対象のリストを取得する(下記は全件取得しているが、適宜絞り込んでOK)
irb(main):004:0* target_records = target_model.all
  DbStatus Load (1.2ms)  SELECT "db_statuses".* FROM "db_statuses"
[
    [0] #<DbStatus:0x007f81c7e78420> {
                 :id => 1,
        :status_name => "hoge",
         :created_at => Sat, 06 Feb 2016 15:39:44 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 15:39:44 UTC +00:00
    },
    [1] #<DbStatus:0x007f81c7e77d18> {
                 :id => 2,
        :status_name => "fuga",
         :created_at => Sat, 06 Feb 2016 15:39:46 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 15:39:46 UTC +00:00
    },
    [2] #<DbStatus:0x007f81c7e77ac0> {
                 :id => 3,
        :status_name => "piyo",
         :created_at => Sat, 06 Feb 2016 15:39:48 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 15:39:48 UTC +00:00
    }
]
irb(main):005:0> write_filepath = "#{Rails.root}/db/seeds/#{target_model.to_s.tableize}.yml"
"/home/vagrant/initDbDataSample/db/seeds/db_status.yml"
irb(main):006:0> open(write_filepath,"w") do |write_file|
irb(main):007:1*   write_file.puts target_records.to_yaml
irb(main):008:1> end
nil
irb(main):009:0> quit

# ここまでで、下準備はOK
# rake db:seedで初期データを登録するため、データはクリーニングしておく
rake db:migrate:reset
```

吐き出したyamlの中身は以下のようになっていてますので、人間の目で見ても、非常にわかりやすいです。

```bash
cat db/seeds/db_status.yml
---
- !ruby/object:DbStatus
  attributes:
    id: 1
    status_name: hoge
    created_at: 2016-02-06 15:39:44.278188000 Z
    updated_at: 2016-02-06 15:39:44.278188000 Z
- !ruby/object:DbStatus
  attributes:
    id: 2
    status_name: fuga
    created_at: 2016-02-06 15:39:46.503348000 Z
    updated_at: 2016-02-06 15:39:46.503348000 Z
- !ruby/object:DbStatus
  attributes:
    id: 3
    status_name: piyo
    created_at: 2016-02-06 15:39:48.697909000 Z
    updated_at: 2016-02-06 15:39:48.697909000 Z
```

上記yamlデータをDBに反映するようなseed.rbは以下です。yamlファイル名は、対応するテーブルのモデル名に変換できることを前提としています。

```ruby:seed.rb
Dir.glob("#{Rails.root}/db/seeds/*.yml").each do |yaml_filename|
  # yamlのファイル名から、対応するモデルクラスを特定し、クラスをロードする
  # 下記行が無いと、「ArgumentError: undefined class/module」が発生する。
  target_model = File.basename(yaml_filename,".yml").classify.constantize
  # すでに登録されているデータを全県削除
  target_model.delete_all

  # yamlに記述されたレコードをDBに登録する。
  File.open(yaml_filename) do |load_target_yaml|
    records = YAML.load(load_target_yaml)

    records.each do |record|
      target_model.create(record.attributes)
    end
  end
end
```

「rake db:seed」できちんと初期データが反映できるか確認してみます。

```bash
# DBがちゃんと初期化できるか確認
rake db:seed

rails c

irb(main):001:0> DbStatus.all
  DbStatus Load (4.3ms)  SELECT "db_statuses".* FROM "db_statuses"
[
    [0] #<DbStatus:0x007f81c6452460> {
                 :id => 1,
        :status_name => "hoge",
         :created_at => Sat, 06 Feb 2016 15:59:11 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 15:59:11 UTC +00:00
    },
    [1] #<DbStatus:0x007f81c6444d60> {
                 :id => 2,
        :status_name => "fuga",
         :created_at => Sat, 06 Feb 2016 15:59:11 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 15:59:11 UTC +00:00
    },
    [2] #<DbStatus:0x007f81c6444a40> {
                 :id => 3,
        :status_name => "piyo",
         :created_at => Sat, 06 Feb 2016 15:59:11 UTC +00:00,
         :updated_at => Sat, 06 Feb 2016 15:59:11 UTC +00:00
    }
]

```

こんな感じで、できてますね！！
ちょっとしたマスタデータを、gitなどでバージョン管理したい場合に役に立つのではないかなと思います。