【実践】Railsにて検索結果をそのままCSV出力する(やや手抜きで)


Webアプリケーションにとって、検索画面は無くてはならないものだと思います。典型的な検索画面といえば、ヘッダー部分に検索条件を入力するフィールドがあり、その下に結果一覧が表示されるもの。

検索画面

さらに顧客の要望としてよく挙がるのが、「検索結果をExcelで見たい」というもの。みんなExcel好きですよねー(私も大好きですが)。

というわけで、CSV出力機能付きの検索画面を(なるべく手抜きで)実装してみよう思います。

筆者の環境

  • Rails 4.0.3 (別に4じゃなくても動くと思います)

Scaffold

まずはCustomerモデルを作成しましょう。サンプルアプリなので、scaffoldで済ましてしまいます。

📄Gemfile
$ rails g scaffold Customer name:string birthday:date sex:string address:string
$ rake db:migrate

準備ができたら、データを適当に登録しておきます。でたらめなデータでよい場合は、私はFakerというgemでテストデータを作っています。参考までに、Faker導入手順も載せておきます。

group :development, :test do
  gem 'faker'
end

bundle install でインストールしたら、fixtureファイルを編集します。

📄customers.yml
<% 100.times do |i| %>
customer<%= i %>:
  name: <%= Faker::Name.name %>
  birthday: <%= Faker::Business.credit_card_expiry_date - 30.years %>
  sex: <%= %W(male female).sample %>
  address: <%= Faker::Address.city %>
<% end %>

編集し終わったら rake db:fixtures:load するとdevelopmentデータベースにデータがロードされます。非常に簡単にテストデータが生成できました。

検索機能を実装する

検索機能も手抜きです。いや、手抜きというか、ransackという素晴らしいgemがあるので、これを利用しましょう。

📄Gemfile
gem 'ransack'

いつも通り bundle install  したら、次はコントローラを編集しましょう。

📄customer_controller.rb
class CustomersController < ApplicationController
  before_action :set_customer, only: [:show, :edit, :update, :destroy]

  def index
    @q = Customer.search(params[:q])
    @customers = @q.result(distinct: true)
  end
...

ransackのGetting started通りの記述です。続いて、Viewに検索フォームを付加します。

📄customers/index.html.erb
<h1>Listing customers</h1>

<%= search_form_for @q do |f| %>
  <%= f.label :name_cont, "Name" %><%= f.text_field :name_cont %>
  <%= f.label :birthday_eq, "Birthday" %><%= f.text_field :birthday_eq %>
  <%= f.label :sex_eq, "Sex" %><%= f.text_field :sex_eq %>
  <%= f.label :address_cont, "Address" %><%= f.text_field :address_cont %>

  <%= f.submit %>
<% end %>

<table>
...

ここまで実装できたら、実際に検索ができるか確認してみましょう。

検索機能

ちゃんと検索できてます。ransackは便利だな・・・

CSV出力機能を実装する

ではいよいよ本題です。検索結果をCSV出力できるようにしましょう。実現方法は色々あると思いますが、今回は「かなり手抜き&汎用的」な方法で実現してみたいと思います。その名も、強引にページ遷移アタックです。

検索のGETリクエストは、画面に表示する場合でもCSVに出力する場合でも、内容は変わりません。URLを見てみましょう。何らかの検索をした上でURLバーを見てください。

URL

Customerコントローラのindexアクションに?以下のパラメータが渡されるという意味ですが、CSV出力の場合も同様の検索処理なので、同じindexアクションでさばくのが良さげです。というわけで、CSV出力時は format: :csv でリクエストが来ると仮定して、indexアクションを変更します。

📄customer_controller.rb
...
  def index
    @q = Customer.search(params[:q])
    @customers = @q.result(distinct: true)
    respond_to do |format|
      format.html
      format.csv { send_data @customers.to_csv }
    end
  end
...

formatによって処理を分けています。format.csvで呼び出される @customers.to_csv はCustomerモデルに実装しています。(本来はプレゼン層の仕事なので、モデルに書くべきでないとは思いますが、今回は単純化のためにモデルに実装しています。)

📄customer.rb
class Customer < ActiveRecord::Base

  def self.to_csv
    CSV.generate do |csv|
      csv << column_names
      all.each do |customer|
        csv << customer.attributes.values_at(*column_names)
      end
    end
  end
end

csvをrequireする必要があるので、config/application.rbも編集します。

📄config/application.rb
require File.expand_path('../boot', __FILE__)

require 'csv'
require 'rails/all'

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env)
...

RailsCastsまんまのコードですが笑。Railsを再起動したらCSV出力ができるはずです。画面で何か検索して結果が表示されたら、そのURLの /customers?utf8=... の部分を /customers.csv?utf8=... に変更してEnterを押してみてください。無事CSVがダウンロードできたでしょうか?

Excelで開くために

ダウンロードしたCSVファイルをExcelで開いてみましょう。上手く表示されている場合と、そうでない場合があると思います。それは『日本語を使用しているかどうか』によります。日本語を使用している場合は、日本語が文字化けし、場合によっては列もずれます。

文字化け

かくも無残な姿。。。これはExcelでUTF8のCSVを扱うには、BOM付きで出力する必要があるからです。少しコントローラを修正しましょう。

📄customer_controller.rb
...
  def index
    @q = Customer.search(params[:q])
    @customers = @q.result(distinct: true)
    respond_to do |format|
      format.html
      format.csv { send_csv @customers.to_csv }
    end
  end
...

send_data メソッドを send_csv メソッドに書き換えました。なんだ、こんな便利なメソッドあったのか・・って無いですよ、もちろん。自力で ApplicationControllerに実装するんです。

📄application_controller.rb
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  protected

    def send_csv(csv, options = {})
      bom = "   "
      bom.setbyte(0, 0xEF)
      bom.setbyte(1, 0xBB)
      bom.setbyte(2, 0xBF)
      send_data bom + csv.to_s, options
    end
end

これでBOM付きUTF8で出力できるはずです。再度CSVを出力し、Excelで開くと・・・

UTF8BOM

グレート。ひとまず、CSV出力については完了です。

CSV出力ボタンを実装する

さて最後のトドメが必要です。CSV出力ボタンを配置しなければなりません。さー、どうやって実装しましょうか?

まず最初に思い付きそうなのが、同じform内にもう一つボタンを配置するという案です。しかしこれだとindexアクションにformat無し(=html)でリクエストすることになるので、コントローラ内で処理を分岐するのが面倒臭そうです。そこで、今回はjavascriptを使用して、同じURLにフォーマットだけを変更して遷移するという、なんとも強引な方法で実装してみたいと思います。

まずは、ヘルパーにCSV出力ボタンのコードを実装します。色んな画面で使い回せそうなので、ApplicationHelperに定義することにしましょう。

📄application_helper.rb
module ApplicationHelper

  def button_to_csv(options = {})
    button_to_function "Export CSV", "reloadWithFormat('csv');", options
  end
end

クリックされると reloadWithFormat というfunctionを呼び出すようにしています。このfunctionはcustomers.js.coffeeにでも定義しましょう。

📄customers.js.coffee
@reloadWithFormat = (format) ->
  url = "#{location.protocol}//#{location.host}#{location.pathname}.#{format}#{location.search}"
  location.href = url

何をしてるかというと、元のURLにフォーマットで指定された文字列を挿入して遷移してるだけ。遷移するといっても、遷移先が send_data してるので、ブラウザ上の表示は変わらないです。では最後に、このボタンをViewに組み込みましょう。

📄customers/index.html.erb
...
  <tbody>
    <% @customers.each do |customer| %>
      <tr>
        <td><%= customer.name %></td>
        <td><%= customer.birthday %></td>
        <td><%= customer.sex %></td>
        <td><%= customer.address %></td>
        <td><%= link_to 'Show', customer %></td>
        <td><%= link_to 'Edit', edit_customer_path(customer) %></td>
        <td><%= link_to 'Destroy', customer, method: :delete, data: { confirm: 'Are you sure?' } %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= button_to_csv %>
...

どこでもいいんですが、私は結果一覧の下に配置しました。

最後に動作を確認おきましょう。適当な条件で検索してExportボタンをクリックすると・・・

Export

完成CSV

無事ダウンロードしたCSVをExcelで開けました。 完成です! お疲れ様でした!

URLを加工して遷移するなんて少々強引な気もしますが、コントローラの処理はスッキリしますし、出力ボタンも使い回しが効きます。単純なアプリなら充分実用に耐えうるのではないでしょうか。

他にもっと良い方法があれば教えて下さいね。

関連する記事


「【実践】Railsにて検索結果をそのままCSV出力する(やや手抜きで)」への1件のフィードバック

  1. ピンバック: モデルのデータを CSV として出力する方法 [Rails] – Site-Builder.wiki

コメントは受け付けていません。