Railsアプリでログ情報などの大量のcsvエクスポート（出力）をWEBアプリ上で行う

# 出力データが大量すぎて時間がかかる
クライアントからログ情報を分析したいとのことでログ情報をDBから出力するような機能を作ったが<br>
実際に実運用が始まってからログを出力した結果、1, 2日くらいのログであれば時間はかかるがcsv出力できた。しかし、全期間になると時間がかかりすぎて使い物にならない。というような事態に陥った。実際に試してみると15分たっても出力されず、「確かにこれは使えないなぁ。。。」という状態だったので、直接SQLを叩いてログ出力をするようにした。<br>

### それまでに使っていた方法

[RailsでDBのデータをCSV出力しよう｜已むに已まれぬ雑記帳｜note](https://note.com/shinryyy/n/nbd2c1d466676)
このような感じでよくあるcsv出力をCSVライブラリを使用して出力していた。（これかな。。
[library csv (Ruby 2.7.0 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/library/csv.html)
）<br>
今回、生SQLでクエリを発行してログ出力するのもCSVライブラリを使っているがちょっとだけ使い方が違う。<br>
`to_sql`メソッドを使ってActiveRecordで発行されるSQLを生SQLに変換して、それを出力している。<br>

`app/services/large_csv_exporter_service.rb`

```ruby
class LargeCsvExporterService
  require 'csv'

  attr_accessor :table, :column, :host, :username, :password, :database

  def initialize(table, column, host, username, password, database)
    @table = table
    @column = column
    @host = host
    @username = username
    @password = password
    @database = database
  end

  def set_file_name(file_name)
    @csv_file_name = file_name
  end

  def set_query(query)
    results = @client.query(query)
    results
  end

  def write_csv(results)
    # File.write(@csv_file_name, encoding: Encoding::SJIS) unless File.exist? @csv_file_name
    CSV.open("./tmp/csv_export/#{@csv_file_name}", "w:sjis") do |csv|
      csv << results.fields
      results.each do |row|
        csv << row.values
      end
    end
  end

  def export_csv_file
    filepath = "./tmp/csv_export/#{@csv_file_name}"
    stat = File::stat(filepath)
    return filepath, stat, @csv_file_name
  end

  def delete_created_csv_file
    if File.exist?("./tmp/csv_export/#{@csv_file_name}")
      File.delete("./tmp/csv_export/#{@csv_file_name}")
    end
  end

  def set_client
    # cache_rows: falseでメモリを使わないようにしている
    @client = Mysql2::Client.new(
      host: host,
      username: username,
      password: password,
      database: database,
      stream: true,
      cache_rows: false,
    )
  end
end
```

serviceをcontrollerのなかで使っている。真ん中あたりの`csv_export = LargeCsvExporterService.new("access_logs", "*", ENV["DB_D_HOST"], ENV["DB_D_USERNAME"], "", ENV["DB_D_NAME"])`らへんから。`ENV["DB_D_HOST"]`とかで、.env（[dotenv](https://github.com/bkeepers/dotenv)）のDB名とかユーザー名とかを参照している。<br>
綺麗なコードは書けないので、大目に見てください。<br>


`app/controllers/admins/access_logs_controller.rb`

```ruby
    def export_csv(params_q)
      @q = AccessLog.order(id: :asc).ransack(params_q)
      user_type_param = params[:q][:user_type].empty? ? ["user", "admin", "supplier"] : params[:q][:user_type].split(':')[1]
      methoda_type_param = params[:q][:method_type] == "ALL_TYPE" ? AccessLog.distinct.pluck(:method_type).compact : params[:q][:method_type]
      access_dates = AccessLog.order(access_date: :asc).to_a
      from_time = params[:q][:created_at_gteq].empty? ? access_dates.first.access_date : params[:q][:created_at_gteq].to_time
      to_time = params[:q][:created_at_lt].empty? ? access_dates.last.access_date : params[:q][:created_at_lt].to_time

      sql_query = AccessLog.where(user_type: user_type_param).where(method_type: methoda_type_param).where(created_at: [from_time..to_time]).to_sql

      csv_export = LargeCsvExporterService.new("access_logs", "*", ENV["DB_D_HOST"], ENV["DB_D_USERNAME"], "", ENV["DB_D_NAME"])
      csv_export.set_file_name("AccessLog_#{Time.zone.now.strftime("%Y%m%d%S")}.csv")
      csv_export.set_client
      # results = csv_export.set_query("SELECT * FROM access_logs WHERE created_at BETWEEN '#{from_time}' AND '#{to_time}' AND method_type = '#{methoda_type_param}' AND user_type = '#{user_type_param}'" )
      results = csv_export.set_query(sql_query)
      csv_export.write_csv(results)
      filepath, stat, csv_file_name = csv_export.export_csv_file
      send_result = send_file(filepath, :filename => csv_file_name, :length => stat.size)
      # csv_export.delete_created_csv_file
    end
```

sqlクエリはこんな感じのが１行で出力される<br>
```sql
SELECT `access_logs`.* 
FROM   `access_logs` 
WHERE  `access_logs`.`user_type` = 'user' 
       AND `access_logs`.`method_type` IN ( 'GET', 'POST' ) 
       AND `access_logs`.`created_at` BETWEEN 
           '2020-08-25 00:00:00' AND '2020-09-27 23:55:35' 
```


まぁ、こんな感じで、実際に時間は計測していないが10万件くらいのファイルでも10秒くらいで出力できるようになった。<br>