LoginSignup
4
3

More than 5 years have passed since last update.

Sowing - データ投入を行うgemについて

Posted at

データ投入を行う gem をとして Sowing というものを新しく作った。
まだ Active Record しか対応していないが、将来的には Ruby だけで動くようにしたい。

動機

Rails の rake db:seed でいい感じにデータを入れたかったが、既存の gem はいまいちだと思う。
例えば seed-fufind_or_create_by で十分だと思うし、 seedbankloadrequire を使えば十分だから必要性を感じない。

sprig という gem が結構良かった。Yaml からデータを投入できるし、必要であれば Ruby でもコードを書ける。こういう小回りが効く感じが良い。
でも sprig は既に投入されているデータかどうかを識別するためのキーを Yaml 中で指定することが嫌だった。しかもこの機能は Yaml でしか使えなかった。sprig そのものは CSV と JSON にも対応しているのに、実質 Yaml 以外ではうまく使えないのだ。

そんなわけでちょっと自分で作ることにした。

方針

大まかにはこうだ。

  • データは CSV か Yaml で定義できる
  • Ruby のコードの中で明示的にデータをロードする (fixturesのように暗黙的にロードしない)
  • データの作成、更新、作成済みであれば無視、の3つができる
  • rake db:seed コマンドに依存しない (seed 以外のデータ投入でも使いたいから)
  • データ(CSV, Yaml)はあくまでもデータして考え、データをどのように取り込むかは呼び出す側のコードで考える (つまり relational data を erb で指定するようなことはしない)

CSVが嫌いな人は多いだろうけど、人類がCSVとExcelから逃げられない現実と向き合う必要がある。
顧客から渡されるマスターデータは十中八九CSVだ。
CSVをYamlに変換するツールは多々あるが、別にCSVから直接データを読み込んで悪いことはない。
忌々しいことにExcelはUTF-8のCSVをうまく処理できないから日本語に場合文字コードは変換する必要があるかもしれないけど、それでもCSVはUTF-8で保存することをおすすめする。
Github上で表として表示してくれるし、googleスプレッドシートならUTF-8でも編集できるからだ。

Sowing の使い方

create メソッド

Sowing は rake db:seed コマンドに依存していないからどこでも使えるけど、ここでは db/seeds.rb の中で使うケースを考えよう。

db/seeds.rb の中身が次であるとする。

require 'sowing'

runner = Sowing::Runner.new

runner.create(User)

Sowing は主に Sowing::Runner のインスタンスに処理を定義してある。
runner.create(User) を実行すると、 Sowing はデータディレクトリ中に users.csvusers.yml がないか探しに行ってファイル中のデータを作成する。
データディレクトリはデフォルトで db/seeds/ だ。

db/seeds/users.csv が次の内容で存在するとする。

first_name,last_name
Carlotta,Wilkinson
中平,薫

first_namelast_nameUser モデルのカラムとして存在していなければならない。
runner.create(User) を実行すると、Carlotta中平 のデータが作成される。

create_or_skip メソッド

Sowing::Runner#create は必ずデータを作成する。
しかし、rake db:seed を複数回実行したときに、既に登録されているデータは無視したい場合がある。
この場合、 create_or_skip メソッドを使う。

require 'sowing'

runner = Sowing::Runner.new

runner.create_or_skip(User, :first_name)

create_or_skipcreate とほぼ同じだが、 第2引数に指定したキーでデータを検索して、見つかった場合には何もせずに次のデータの処理にうつる。
これによりデータファイル(CSVかYaml)中に定義したデータは1度しかDBに取り込まれない。

create_or_update メソッド

require 'sowing'

runner = Sowing::Runner.new

runner.create_or_update(User, :first_name)

create_or_updatecreate_or_skip とほぼ同じだが、名前の通り、登録しようとしたデータがDBで見つかった場合、スキップせずに値を更新する。

Relational data

他のテーブルに関連するデータを投入する機能もある。
(この機能は作ったばかりだからもし動かなかったら教えてほしい。)

例えば、次の内容の db/seeds/profiles.csv が存在する。

user_id,address,phone
"first_name: Carlotta","2001 N Clark St, Chicago, IL 60614 America","+1 111-222-3333"
"first_name: 中平","東京都新宿区新宿1-1-1","090-1111-2222"

これを処理するためのコードは次:

require 'sowing'

runner = Sowing::Runner.new

runner.create(User)

runner.create(Profile) do
  mapping :user_id do |cel|
    User.find_by!(
      first_name: cel['first_name']
    ).id
  end
end

まず、 runner.create(User) で普通に User データを作成する。
次の runner.create(Profile) には block がついている。
block の中では mapping メソッドが使える。
mapping メソッドは指定したカラム名の入力しの変換ルールを定義することができる。

profiles.csv の各行を処理する時、 mapping で定義した変換ルールが適用される。
例えば、 profiles.csvuser_id の最初のデータは "first_name: Carlotta" だ。
mapping の block の第1引数(上記のコードでは cel)にはデータファイルの文字列が Hash に変換されて渡されている。
つまり、文字列の "first_name: Carlotta" が Hash の {"first_name" => "Carlotta"} として cel に格納される。
mapping の block では、データファイル上の情報からDBに登録するときに必要な情報に変換するコードを書く必要がある。
この場合、 cel['first_name'] を使って User の first_name が 'Carlotta' に一致するデータを探して、その ID を返す。
これにより、 Profile を DB に登録する際には、 profile.user_id = User.find_by!(first_name: 'Carlotta').id と同じような処理が行われることになる。

環境毎に seed データを変更する

これは Sowing と直接は関係ないが、よく使うテクニックなので書いておく。
Rails.env に応じて seed データを変更したいケースはよくある。
これには特別な gem は必要ない。

まずディレクトリ構造:

db
├── seeds/
│   ├── development/
│   ├── production/
│   ├── test/
│   ├── development.rb
│   ├── production.rb
│   └── test.rb
└── seeds.rb

で、 db/seeds.rb の中を下記にする:

# db/seeds.rb
path = Rails.root.join('db', 'seeds', "#{Rails.env}.rb")
load path if path.exist?

これで、環境に応じて db/seeds/development.rbdb/seeds/production.rb などを使うことができるようになった。

ただし、 Sowing のデフォルトのデータディレクトリは db/seeds なので、次のようにする必要がある。

# db/seeds/development.rb
require 'sowing'

sowing = Sowing::Runner.new(data_directory: 'db/seeds/development')

sowing.create(User)

まとめ

データ投入なんて多少コードが汚くても別に良いんだけど、ごちゃごちゃしてくるとどういうデータができるのかよくわからないし、データ投入に FactoryGirl とかを使うのも嫌だし、こういうYamlやCSVからDBにデータを取り込めるだけのちょっとした gem があると便利なんじゃないかと思う。

残念ながらまだ ActiveRecord でしか動かないから、今後はもっと Pure Ruby で動くようにしていきたい。 ActiveRecord に依存した部分は Strategy パターンで切り出してあるから、おそらくそのうち他の gem に切り分けると思う。

正直テストもそんなに書いてないからうまく動かないことがあるかもしれないけど、 issue とかで教えてくれたら嬉しい。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3