👌

gitlabリポジトリでのCSVエクスポートのやりかた。

2024/12/03に公開

https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/historical_user_data/csv_service.rb

headers_to_value_hashでCSVヘッダとbodyのデータ変換マッピングを用意する。

    def header_to_value_hash
      {
        'Date' => ->(historical_datum) { historical_datum.recorded_at.utc.to_fs(:csv) },
        'Billable User Count' => 'active_user_count'
      }
    end

https://gitlab.com/gitlab-org/gitlab/-/blob/master/gems/csv_builder/lib/csv_builder/builder.rb
CSVBuilderでheader_to_value_hashを利用して変換する。

    def row(object)
      attributes.map do |attribute|
        data = if attribute.respond_to?(:call)
                 attribute.call(object)
               elsif object.is_a?(Hash)
                 object[attribute]
               else
                 object.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend -- Not user input
               end

検証用スクリプト

# csv.rb
require 'active_record'
require 'sqlite3'

# ロギング
ActiveRecord::Base.logger = Logger.new($stdout)

# データベース接続の設定
ActiveRecord::Base.establish_connection(
  adapter: 'sqlite3',
  database: 'db/development.sqlite3'
)

# テーブルの作成
ActiveRecord::Schema.define do
  drop_table :historical_data, if_exists: true
end

ActiveRecord::Schema.define do
  create_table :historical_data do |t|
    t.integer :active_user_count
    t.datetime :recorded_at
    t.timestamps
  end
end

class HistoricalData < ActiveRecord::Base
end

require 'csv'
class CsvBuilder
  def initialize(records, header_to_value_hash)
    @records = records
    @header_to_value_hash = header_to_value_hash
  end

  def render
    CSV.generate do |csv|
      csv << @header_to_value_hash.keys
      @records.each do |record|
        csv << @header_to_value_hash.values.map do |attribute|
          if attribute.is_a?(Proc)
            attribute.call(record)
          else
            record.send(attribute)
          end
        end
      end
    end
  end
end

module HistoricalUserData
  class CsvService
    def initialize(historical_data_relation)
      @historical_data_relation = historical_data_relation
    end

    def generate
      csv_builder.render
    end

    def csv_builder
      CsvBuilder.new(@historical_data_relation, header_to_value_hash)
    end

    def header_to_value_hash
      {
        'Date' => ->(historical_datum) { historical_datum.recorded_at.utc.to_fs(:csv) },
        'Billable User Count' => 'active_user_count'
      }
    end
  end
end

HistoricalData.create(active_user_count: 1, recorded_at: "2024-01-01 00:00:00")
HistoricalData.create(active_user_count: 2, recorded_at: "2024-01-02 00:00:00")

puts HistoricalUserData::CsvService.new(HistoricalData.all).generate
Date,Billable User Count
2024-01-01 00:00:00 UTC,1
2024-01-02 00:00:00 UTC,2

header_to_value_hashを変更することで柔軟にCSVフォーマットを変更することが可能。
たとえばCustomDateを追加する場合は以下。

    def header_to_value_hash
      {
        'Date' => ->(historical_datum) { historical_datum.recorded_at.utc.to_fs(:csv) },
        'CustomDate' => ->(historical_datum) { custom_recorded_at(historical_datum) },
        'Billable User Count' => 'active_user_count'
      }
    end

    def custom_recorded_at(historical_datum)
      historical_datum.recorded_at.utc.strftime('%Y年%m月%d日')
    end
Date,CustomDate,Billable User Count
2024-01-01 00:00:00 UTC,2024年01月01日,1
2024-01-02 00:00:00 UTC,2024年01月02日,2

まとめ

gitlab内の内部Gem CSVBuilderを利用してCSV出力処理をしている。
https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/csv_builder

参考

Ruby: CSVでヘッダとボディを同時に定義するやり方も参照
https://techracho.bpsinc.jp/kazz/2019_07_30/78334

CSVBuilderではpublic_sendも利用してCSV出力処理をしている違いがある。

Discussion