背景
とあるサービスをrailsで作っていたが、初期データが多くてseedにすべて書くのは大変&エンジニア以外にも手伝ってもらいたかったので、csvファイルでseedデータを作ることにしました。
csvファイルをseed.rbで読み込む方法はすでに存在しますが、csvファイル1つずつ読み込んだり、一括で読み込むものもvalidationを無視するものが多いです。かといって、validationを守りつつcsvファイルを読み込むためには、読み込む順番を制御しなければなりません。
コードを書いてると60行ぐらいになり、rubyのメタプログラミング的手法(カッコイイ)も使ったので、練習も兼ねてgem化することにしました。
成果物
https://github.com/aitaro/csv_seeder
(よかったら使ってね)
他のcsv読み込み系qiita
無限にあったので検索結果から探してください。
準備
RubyGemsの作り方を参考に初期設定をしていきます。
ちなみにgem名はcsv_seederにしました。
設計
まず、想定するcsvファイルは下のようなものです。(csvにidまで含めているのは、relationで指定することができるからです。)
id,name,status
1,プログラミングの書き方,release
2,rails基礎,draft
id,post_id,body,rate
1,1,よく分かった,5
2,1,少し分かった,4
3,2,わからなかった,3
基本的な設計方針として、csvファイルを順番に読み込みつつ、依存関係(belongs_to等)によりvalidationに引っかかるcsvファイルを後回しにします。
例えば以下のようなモデルのとき、
class Post
has_many :comments
end
class Comment
belongs_to :post
end
先にCommentを読み込むと、指定したPostがないよと怒られます。なのでPostを先に読み込んでからComnmentを読まないいけません。
このように"validationに引っかかるもの"のほとんどはbelongs_toによるものです。
実装
基本ループの実装です。
def initialize(folder_path, orders)
@folder_path = folder_path
@orders = orders
@dirs = Dir.glob(folder_path + "/*").shuffle
end
def dirs_loop!
while !@dirs.empty?
if invalid_order?
postpone!
next
end
if has_relations?
postpone!
next
end
/db\/seeds\/(\w*)\.csv/ =~ @dirs[0]
model = $1.classify
CSV.foreach(@dirs[0], headers: true) do |row|
Object.const_get(model).create!(**row.to_hash)
rescue => e
p "Error! During #{model}"
p e
end
p "#{model} saved!"
@dirs.shift
end
end
@dirs
にcsvのpath一覧が入っています。これからinvalid_order?
やhas_relations?
等の条件を適合したものだけをcreateしています。
CSV.foreachによりcsvファイルの読み込み、Object.const_get(model)により動的にクラスを呼び出しています。
has_relations?の実装です。親クラスがまだ読み込まれていない場合は後回しにします。
def has_relations?
CSV.open(@dirs[0], &:readline).any? do |header|
id_list = @dirs.map do |dir|
dir_to_plural(dir).singularize + '_id'
end
id_list.include? header
end
end
invalid_order?の実装です。@orders
はhas_relations?で補足できない依存関係を、ユーザーからのインプットパラメータとして受け取ったものです。@orders
に禁止するものを後回しにしています。
def invalid_order?
@orders.each do |order|
index = order.index(dir_to_plural(@dirs[0]).to_sym)
next if !index || index == 0
return true if @dirs.any?{|dir| order[0..(index-1)].map(&:to_s).include? dir_to_plural(dir)}
end
false
end
所管
簡単なgemですが、公開するとやりきった感があってたのしいです。なにか提案等あればこの記事のコメントかissueかに書いてください。(反応できるかわからないですが、、)
TODO
個人的メモです。ここまで書いて力尽きました。以下のことはまた今度やります。
- gem使い方の説明を書く
- 依存関係をはっきりさせる。
- gemのtestコードを書く