Railsアプリでログ情報などの大量のcsvエクスポート(出力)をWEBアプリ上で行う
出力データが大量すぎて時間がかかる
クライアントからログ情報を分析したいとのことでログ情報をDBから出力するような機能を作ったが
実際に実運用が始まってからログを出力した結果、1, 2日くらいのログであれば時間はかかるがcsv出力できた。しかし、全期間になると時間がかかりすぎて使い物にならない。というような事態に陥った。実際に試してみると15分たっても出力されず、「確かにこれは使えないなぁ。。。」という状態だったので、直接SQLを叩いてログ出力をするようにした。
それまでに使っていた方法
RailsでDBのデータをCSV出力しよう|已むに已まれぬ雑記帳|note
このような感じでよくあるcsv出力をCSVライブラリを使用して出力していた。(これかな。。
library csv (Ruby 2.7.0 リファレンスマニュアル)
)
今回、生SQLでクエリを発行してログ出力するのもCSVライブラリを使っているがちょっとだけ使い方が違う。
to_sql
メソッドを使ってActiveRecordで発行されるSQLを生SQLに変換して、それを出力している。
app/services/large_csv_exporter_service.rb
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)のDB名とかユーザー名とかを参照している。
綺麗なコードは書けないので、多めに見てください。
app/controllers/admins/access_logs_controller.rb
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クエリはこんな感じのが1行で出力される
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秒くらいで出力できるようになった。