LoginSignup
38

More than 3 years have passed since last update.

railsでCSV出力をする方法+実務あるある(関連テーブルのカラム出力など)

Last updated at Posted at 2019-07-03

社内用の顧客管理ツールを作った際に、バックオフィスのメンバーから「CSV出力機能がほしい!」という熱い要望がありました。

CSV出力機能はよくある機能ですが、実装後も細かな要望に答えて改修したので実務あるあるとしてまとめました。

前提のお話

前提としてtownは複数のuserを持ち、userは1つのtownと紐づきます。
特定のtownごとのuserをまとめてCSV出力するボタンを作りました。

models/town.rb
class Town < ApplicationRecord
  has_many :users
end
models/user.rb
class User < ApplicationRecord
  belongs_to :town
end
towns/index.html.erb
<div class="flex space-between">
   <div>
     <% @towns.each do |town| %> 
       <p><%= town.name %></p>
       <%= link_to "CSV出力", towns_csv_town_user_path(id: town.id) %>
     <% end %>
   </div>
</div>

userテーブル

id town_id last_name first_name address email
1 1 田中 太郎 〇〇町 tanaka@gmail.com
2 2 佐藤 花子 △△町 sato@gmail.com

townテーブル

id name population food tourist spot
1 名古屋 300000 味噌カツ 名古屋港水族館
2 札幌 200000 カニ 時計台
config/routes.rb
  resources :towns do
    get "csv_town_user", on: :member
  end

ボタンを押した時にtowns_controllerのcsv_town_userメソッドにtown.idを渡して、townごとのuserをCSV出力します。

基本のCSV出力

towns_controller.rb
require "csv"

class TownsController < ApplicationController

  def index
    @towns = Town.all
  end

  def csv_town_user
    head :no_content

    users = User.where(town_id: params[:id])
    town = Town.find(params[:id])
    #ファイル名を指定 ここはお好みで
    filename = town.name + Date.current.strftime("%Y%m%d")

    csv1 = CSV.generate do |csv|
      #カラム名を1行目として入れる
      csv << User.column_names

      users.each do |user|
        #各行の値を入れていく
        csv << user.attributes.values_at(*User.column_names)
      end
    end
    create_csv(filename, csv1)
  end

  private
    def create_csv(filename, csv1)
      #ファイル書き込み
      File.open("./#{filename}.csv", "w", encoding: "SJIS") do |file|
        file.write(csv1)
      end
      #send_fileを使ってCSVファイル作成後に自動でダウンロードされるようにする
      stat = File::stat("./#{filename}.csv")
      send_file("./#{filename}.csv", filename: "#{filename}.csv", length: stat.size)
    end
end

実務あるある1: 一部のカラムだけCSV出力したい

towns_controller.rb
require "csv"

class TownsController < ApplicationController
  #略

  def csv_town_user
    head :no_content

    users = User.where(town_id: params[:id])
    town = Town.find(params[:id])
    filename = town.name + Date.current.strftime("%Y%m%d")

    csv1 = CSV.generate do |csv|
      #ここでカラム指定
      columns = ["town_id", "last_name", "first_name", "address", "email" ]
      csv << columns
      users.each do |user|
        csv << user.attributes.values_at(*columns)
      end
    end
    create_csv(filename, csv1)
  end

  private
  #以下略
end

実務あるある2: 1行目はカラム名を日本語にしたい

towns_controller.rb
require "csv"

class TownsController < ApplicationController
  #略

  def csv_town_user
    head :no_content

    users = User.where(town_id: params[:id])
    town = Town.find(params[:id])
    filename = town.name + Date.current.strftime("%Y%m%d")

      #日本語のカラム名を用意
      columns_ja = ["都市ID", "名字", "名前", "住所", "メールアドレス"]
      columns = ["town_id", "last_name", "first_name", "address", "email" ]

    csv1 = CSV.generate do |csv|
      #1行目は日本語のカラム名
      csv << columns_ja
      users.each do |user|
        csv << user.attributes.values_at(*columns)
      end
    end
    create_csv(filename, csv1)
  end

  private
  #以下略
end

実務あるある3: テーブル結合して別テーブルの情報も表示したい

現在は各userの"town_id", "name", "address", "email" が出力されるが、"town_id"ではなく、townテーブルを結合してtownテーブルのnameカラムを出力したいとします。

towns_controller.rb
require "csv"

class TownsController < ApplicationController
  #略

  def csv_town_user
    head :no_content
    #ここでtownテーブルの情報を同時に引いてくる
    users = User.where(town_id: params[:id]).includes(:town)
    filename = town.name + Date.current.strftime("%Y%m%d")

      #都市IDではなく都市名を表示するようにする
      columns_ja = ["都市名", "名字", "名前", "住所", "メールアドレス"]
      columns = ["town_name", "last_name", "first_name", "address", "email" ]

    csv1 = CSV.generate do |csv|
      csv << columns_ja
       users.each do |user|
        user_attributes = user.attributes 
       #user.attributesオブジェクトにtown_nameというキー名でtownテーブルのnameカラムを追加する
       #includesで結合しても呼び出すときはuser.nameではなくuser.town.nameなので注意
        user_attributes["town_name"] = user.town.name
        csv << user_attributes.values_at(*columns)
      end
    end
    create_csv(filename, csv1)
  end

  private
  #以下略
end

7/7追記

コメントを頂いたのでcsv作成部分をcontrollerからviewsフォルダ内の.rbファイルに切り分けたパターンも書いてみました!controllerの記述量が減って良い感じです。
ちなみに、コメントではrespond_toで呼ばれるformatごとに呼び出すviewファイルを分ける方法を教えていただきましたが、今回出力したかったのはviews/index.html.erb上で選択した都市ごとのcsvファイルだったので、indexのformat切り替えの方法ではなくメソッドを分けることにしました。

index.html.erbではformat: :csvと書くことで、views/towns/csv_user.rubyを読み込むようにします。
(微妙にpathも変更しました)

towns/index.html.erb
<div>
   <div>
     <% @towns.each do |town| %> 
       <p><%= town.name %></p>
       <%= link_to "CSV出力", csv_user_town_path(id: town.id, format: :csv) %>
     <% end %>
   </div>
</div>

routesはこんな感じです

config/routes.rb
Rails.application.routes.draw do
  resources :towns, only: [:index, :new, :create] do
    get 'csv_user', on: :member
  end
  get 'users/new'
end

今まではcontrollerにcsvファイル作成処理を書いていましたが、新しくviews/towns/csv_user.rubyを作成してcsv出力処理をここに切り分けました。

views/towns/csv_user.ruby
require "csv"

@users = User.where(town_id: params[:id])
town = Town.find(params[:id])


CSV.generate do |csv|
  csv << User.column_names
  @users.each do |user|
    csv << user.attributes.values_at(*User.column_names)
  end
end

↑ここで作られたcsvはcontroller内のrender_to_stringで取得されます。
send_fileからsend_dataにすることで、ファイルそのものではなくデータのみを渡すようになり、File.openで最初にファイルを作成する必要もなくなりました。

towns_controller.rb
require "csv"

class TownsController < ApplicationController

  def index
    @towns = Town.all
  end

  def csv_town_user
    head :no_content
    send_data(render_to_string, type: "text/csv")
  end
end

エンコードエラー対策やi18n導入はまだできていませんが、ひとまずcontrollerの記述を減らす部分まで...
scivolaさんありがとうございます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38