Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby on Rails: DBからの取得結果をいい感じにCSVにして出力を実装したいMEMO

普段、CSV出力系の機能を作ることが稀に良くあるが、割と毎回同じようなことを実装してる気もするのでいい感じに共通化できないか実装を検討したのでメモ

ベンチマークは特に取ってない

CSV出力の実装検討メモ

考えたこと

  • CSV出力時にエラーが発生した場合に、ハンドリングしやいようにカスタムエラーをraiseする
  • 各行の出力ロジックは算出値を利用することもあるので外部から注入できるようにする
  • ActiveRecord::Relationからよしななオブジェクトを返す
    • in_batchesで取得してDB負荷を抑制
  • アプリケーション側のメモリ負荷抑制のためにCSVを分割して出力する(スコープ外)
    • 直接send dataでレスポンスとして返すようなケースもありそうなので今回はスコープ外
  • よしなにBOMをつける
    • エクセルは対応しなくてもいいケースもありそうなので今回はスコープ外

実装してみる

module Csv
  class Genarator
    def initialize(relation:, headers:, row_storategy:, batch_size: 1000)
      @relation = relation
      @headers = headers
      @row_storategy = row_storategy
      @batch_size = batch_size
    end

    def call!
      raw_rows = genarate_raw_rows(@relation, @row_storategy, @batch_size)
      rows = genarate_rows(@headers, raw_rows)
      genarate_csv(rows)
    rescue StandardError => error
      raise GenarateFailed, error
    end

    private

    def genarate_raw_rows(relation, strategy, batch_size)
      relation.in_batches(of: batch_size)
              .reduce([]) { |result, records| result + strategy.call(records) }
    end

    def genarate_rows(headers, raw_rows)
      generate_row = ->(raw_row) { CSV::Row.new(headers, raw_row) }
      raw_rows.map(&generate_row)
    end

    def genarate_csv(rows)
      CSV::Table.new(rows)
    end
  end
end

補足

  • row_storategyでProcオブジェクトを渡すことで行生成のロジックを外部から注入できるようにした
    • 本当だったらpluckとかその辺りの最適化のロジックも良しなに組み込みたかったけど、そこは責務から除外した。この辺を矯正したいならclumnsみたいなのを引数で撮ってrelationにpluckで適用するような処理を内部に持つようにしても良いかもしれない。
  • in_batches + reduceで一定件数ずつ抽出 + 処理して結果を返すようにした
  • CSV::Rowを生成してCSV::Tableを返すようにした
    • この辺はpureなHashとかArrayを生成してCSV.generateした方が負荷的には良いのかもしれない

利用例

headerと行の抽出ロジックを渡せば良しなに生成してくれる。

headers = %i[id title endpoint last_published_at created_at updated_at]
row_storategy = ->(records) { records.pluck(*headers) }
result = Csv::Genarator.new(relation: feeds, headers: headers, row_storategy: row_storategy).call!
result.to_csv # CSV文字列を返す

参考

docs.ruby-lang.org

docs.ruby-lang.org