この記事は食べログ Advent Calendar 2019 7日目の記事です。
よくあるデータ出力機能をふたつに分けて作ったら、機能の汎用性がとても高くなったはなしをします。
おそらくよくあるデータ出力処理
ふだん、データ出力ってどのように作りますか?
自分は、こんな感じで
レコードの絞り込みとデータの取得を同時に行なって
1レコードずつ出力したい順に情報を並べ替えてファイルに出力
することが多いです。
records = Employee.select(:age, :first_name, :last_name).where(job: :engineer)
CSV.open("engineer.csv") do |csv|
csv << [:first_name, :last_name, :age] # headers
records.each do |record|
csv << [record.first_name, record.last_name ,record.age]
end
end
いつものようにしたけれど、コードが長くなっちゃった
その日は出力したい情報の種類が多いデータ出力を作っていました。
いつものように作ってみたけれど、データの取得先だけで5種類もあってコードがとても長い、、、
なんとかしたい。
そのときふと
出力したいレコードの絞り込みと、出力したいデータの取得処理を
分けてみたらどうだろうと思いました。
レコードの絞り込みと、データの取得をわけてみた
そうして出来上がったのがこちらです。
欲しいレコードの検索条件と
知りたいデータの項目名を定義したファイルを渡します。
id | name | tel | address | station | url | reviews_url | yoyaku |
---|
するとまず、レコード絞り込み機能が
レコードの特定条件で絞り込み検索をして出力対象レコードのidのみを出力します。
id | name | tel | address | station | url | reviews_url | yoyaku |
---|---|---|---|---|---|---|---|
100 | |||||||
200 | |||||||
300 |
続いて、このファイルがデータ取得機能に渡されて
id | name | tel | address | station | url | reviews_url | yoyaku |
---|---|---|---|---|---|---|---|
100 | カフェ | 03-0000-0000 | 恵比寿南 | 恵比寿駅 | /100/ | /100/dtlrvwlst | なし |
200 | ラーメン屋 | 03-0000-0000 | 恵比寿南 | 恵比寿駅 | /200/ | /200/dtlrvwlst | なし |
300 | イタ飯屋 | 03-0000-0000 | 恵比寿南 | 恵比寿駅 | /300/ | /300/dtlrvwlst | あり |
idをキーにデータを取ってきて、項目名列に対応した情報を付加してくれます。 |
表の項目名は、データ取得先ごと色分けしてみました。
データ取得先はDBだったりAPIだったりするので、単純に結合もできず
特にAPIはデータを取るときにページングや並列処理をしたりもするのでわりと手間がかかります。
データ取得機能の汎用性がとてもよかった
長いコードを整理するために機能を分割したのですが
思いがけず、データ取得機能の汎用性がとても高いことに気づきました。
たとえば
レコードを絞り込む条件は違うけど、出力したいデータが似ている機能を新しく作りたいときは、
レコードの絞り込み部分だけ作れば、あとはデータ取得機能に渡すだけ!
あるいは
既にレコードは特定できていて、詳細情報を知りたいとき
id列を埋めたファイルをデータ取得機能に渡すだけ!
**渡すだけ!**これはなかなか汎用性があるぞう。
単純なデータ出力ではあまり恩恵はなさそうですが
今回ご紹介したように、データ出力のために用意する種類が多くて手間がかかるケースで
それを利用したいシーンが複数あると、たくさんの恩恵を受けられそうです。
ドメイン間は疎結合のまま、情報満載なデータ出力ができそうな予感
今回はひとつのドメインのデータ取得機能だけ作りましたが、別のドメインにもデータ取得機能を作り
知りたいデータの項目定義ファイルに、いろんなドメインのデータ取得機能を旅させることで
情報満載なCSVファイルを作ることもできそうです。
感想
ふだん何気なくやっていたことを少し変えてみたら新しい発見につながったので
今後も何気ないことを改めて考えてみて、いろんな発見をしていきたいですね!
明日は @hiroteru_ さんによる「UIViewの角丸と影のおはなし」です。
お楽しみに!!
最後までご覧いただきありがとうございました!
おまけ
データ出力機能を作るときに役立ちそうなコード集です。
最近のExcelってUTF-8を理解できるんですって(ただしBOMつきに限る)
最近のExcelは、UTF-8でもBOMつきであれば文字化けせずにcsvファイルを開いてくれるようです。
File.write(filepath, "\xEF\xBB\xBF") # excelで直接UTF-8を開けるようにするためにBOMをつける
CSV.open(filepath, 'a') do |csv| # 追加書き込みモードで開く
これで「ファイルをインポート」機能とお別れできます。
この方法で作成したCSVを読み込むときは、BOMついてるかもよ。と教えてあげる必要があります。
CSV.open(filepath, encoding: "BOM|UTF-8") do |csv|
データ取得機能で活躍した遅延ロード
不必要なデータまで取得していると、処理に無駄な時間がかかってしまいます。
1つの取得先のデータだけあれば済むときに、5つの取得先からデータを取得したくないですよね。
こんなときに遅延ロードが大活躍しました。
class Hoge
def initialize(id)
@id = id
end
def name
data.name
end
def tel
data.tel
end
private
def data
return @data if defined? @data # メモ化
@data = HogeData.select(:name, :tel).where(id: @id).take
end
end
header = csv.headers # 出力したいと指定されている項目名を取得
-> ["name", "tel"]
hoge = Hoge.new(id)
header.each do |column|
hoge.public_send(column.to_sym) # 初回にnameが呼び出されたときに、中のdataメソッドがデータを取得してくれる
# 続いて tel が呼び出されたときは、メモ化された@dataを見るので改めてデータ取得はしない
end
このように、nameの情報を出力したい。と呼び出されたときにはじめてデータを取得することで不必要なデータは取得せずに済むようになりました。
あるクラスのメソッドをまとめてdelegateに定義する
delegateって明示的にメソッド名を定義する必要があって、面倒ですよね。
Hogeクラスのメソッド全部をdelegateしたいときは、こんな書き方で定義できました。
delegate *Hoge.instance_methods(false), to: :hoge
データ取得機能では、データの取得先毎にクラスをわけてオブジェクトを作っています。
メソッドが呼び出されたら、そのデータを持っているオブジェクトにdelegateするように、この方法で実現しています。