データベースからテストフィクスチャを抽出する(to_yaml 不使用)

ar_fixtures の出力はなんだかダサくない?

データベースから yaml 形式のフィクスチャを抽出するプラグインとしては、ar_fixtures がある。さっきインストールして使ってみたけど、どうも yaml の出力が気に入らない。たとえば次のような感じになってしまう。

---
- !ruby/object:Entry
  attributes:
    title: MyString1
    body: MyText1
    id: "1"
- !ruby/object:Entry
  attributes:
    title: MyString2
    body: MyText2
    id: "2"

- !ruby/object:Entry ってなんだかよくわからないのがついてるし、カラムの表示順がランダムになってしまっていて、見づらい。次のような出力がほしいのだ。

entry1:
  id: 1
  title: MyString1
  body: MyText1

entry2:
  id: 2
  title: MyString2
  body: MyText2

あと、ar_fixtures は内部的に to_yaml というメソッドを使っていて、これが UTF-8 の文字列をうまく扱えない。そこで、

日本語をto_yamlするとエンコードされてしまう問題を安直な方法で解決する
to_yamlでUTF-8な日本語がbinaryになってしまう問題を回避するRailsプラグイン

みたいな hack が必要になってくる。

そんなわけで、Chad Flower「Rails レシピ」のレシピ41「生データからのテストフィクスチャの抽出」(p155) のソースコードをベースに次のような Rake タスクを作ってみた。to_yaml は使ってないので、UTF-8 文字列にまつわる頭痛とも無縁だ。興味があれば、使ってみてほしい。

使い方

下のコードを丸ごとコピーして、extract_fixtures.rake というファイルを作り、$RAILS_ROOT/lib/tasks の直下に置く。すると rake db:fixtures:extract という新しい Rake タスクが使えるようになる。

% rake db:fixtures:extract 
# => すべてテーブルの内容が tmp/fixtures/*.yml に抽出される
% rake db:fixtures:extract FIXTURES=entries,comments
# => テーブル entries, comments の内容のみが tmp/fixtures/*.yml に抽出される

という感じで使えるはずだ。

ソースコード

def fixture_entry(table_name, obj)
  res = []
  klass = table_name.singularize.camelize.constantize
  res << "#{table_name.singularize}#{obj['id']}:"
  klass.columns.each do |column|
    res << "  #{column.name}: #{obj[column.name]}"
  end
  res.join("\n")
end
   
namespace :db do
  fixtures_dir = "#{RAILS_ROOT}/tmp/fixtures/"
  namespace :fixtures do
    desc "Extract database data to the tmp/fixtures/ directory. Use FIXTURES=table_name[,table_name...] to specify table names to extract. Otherwise, all the table data will be extracted."
    task :extract => :environment do
      sql = "SELECT * FROM %s ORDER BY id"
      skip_tables = ["schema_info"]
      ActiveRecord::Base.establish_connection
      FileUtils.mkdir_p(fixtures_dir)

      if ENV['FIXTURES']
        table_names = ENV['FIXTURES'].split(/,/)
      else
        table_names = (ActiveRecord::Base.connection.tables - skip_tables)
      end

      table_names.each do |table_name|
        File.open("#{fixtures_dir}#{table_name}.yml", "w") do |file|
          objects  = ActiveRecord::Base.connection.select_all(sql % table_name)
          objects.each do |obj|
            file.write  fixture_entry(table_name, obj) + "\n\n"
          end
        end
      end
    end
  end
end