UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

【Rails】Rubyテンプレートで axlsx を出力する方法

ハローこんにちは、新人エンジニアのハトネコエです。

2016年10月22日発売の 『WEB+DB PRESS Vol.95』
UUUM のエンジニア達が書いた特集『試して学ぶHTTP』が載ります。よろしくお願いします。

WEB+DB PRESS Vol.95

WEB+DB PRESS Vol.95

  • 作者: 小出淳子,黒澤剛志,牧大輔,横江亮佑,山口貴也,尾藤正人,佐藤琢哉,中橋研太郎,田中慎司,小西裕介,伊藤直也,稲富駿,前島真一,長野雅広,山際康貴,のざきひろふみ,うらがみ,岡林大,遠藤雅伸,ひげぽん,海野弘成,はまちや2,竹原,大場寧子,大場光一郎,野々下裕子,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2016/10/22
  • メディア: 大型本
  • この商品を含むブログを見る

Rubyテンプレート

Rails4になってcsvが出力しやすくなった - Qiita

ここの記事にあるように、view テンプレートとして .ruby ファイルが使えるようになっています。
長くなりがちな CSV 出力の処理を controller 内でなく別ファイルに書けるので controller がすっきりしますね。

Excel 出力もこんなふうにしたい

Excel で表データを扱うことを前提にすると、CSV では、文字コードとか先頭に0を詰める出力がつらい。
CSV でなく Excel 用の .xlsx 形式で書き出したいよねーというわけで、
お世話になっているのが Axlsx という gem。

これで上記のような方法で controller と出力処理を分離しつつエクスポートする方法を探りました。

Before

もとは controller 内にこんなコードを書いていました。

require "axlsx"

class UsersController < ApplicationController
  def export
    Axlsx::Package.new do |p|
      filename = "users_list_" + Time.zone.now.to_date.to_s
      styles = load_styles(p.workbook)

      p.workbook.add_worksheet(name: filename) do |sheet|
        sheet.add_row %w(ID メールアドレス ユーザー名), style: styles[:column_title]

        User.find_each do |user|
          sheet.add_row [
            user.id,
            user.email,
            user.name,
          ], style: [
            styles[:fill_zero],
            styles[:default],
            styles[:default],
          ]
        end
      end
      send_data(
        p.to_stream.read,
        type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        filename: filename + ".xlsx"
      )
    end
  end

  private

    # セルの書式設定を読み込む
    def load_styles(workbook)
      default_font_family = "MS Pゴシック"

      styles = {}
      styles[:default] = workbook.styles.add_style(font_name: default_font_family)
      styles[:column_title] = workbook.styles.add_style(font_name: default_font_family, b: true, alignment: { horizontal: :center }, bg_color: "f9ef93")
      styles[:fill_zero] = workbook.styles.add_style(font_name: default_font_family, format_code: "0000000000")
      styles
    end
end

そして view に

<%= link_to "ユーザーリスト出力", export_users_path %>

とリンクがある形。

After

このように変えました。
(コードの動きがわかりやすいよう、実際に実装した順番とはやや異なります)

1. MIME-TYPE を記述

config/initializers/mime_types.rb に xlsx 形式の MIME-TYPE を記述します。
なお、initializers の設定ですので、rails server を実行中の場合は記述後に再起動してください。

Mime::Type.unregister :xlsx
Mime::Type.register "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", :xlsx

2. 書式設定の読み込みは Service へ

セルの書式設定は使いまわしますので、
書式設定を読み込むメソッドは、AxlsxSupportService を作り、収めました。

class AxlsxSupportService
  # セルのカスタム書式設定を読み込む
  def load_styles(workbook)
    default_font_family = "MS Pゴシック"

    styles = {}
    styles[:default] = workbook.styles.add_style(font_name: default_font_family)
    styles[:column_title] = workbook.styles.add_style(font_name: default_font_family, b: true, alignment: { horizontal: :center }, bg_color: "f9ef93")
    styles[:fill_zero] = workbook.styles.add_style(font_name: default_font_family, format_code: "0000000000")
    styles
  end
end

3. Rubyテンプレートの作成

views/user ディレクトリ内に index.xlsx.ruby を作成しました。
呼び出されると、Axlsx::Package クラスである変数 package を返します。

require "axlsx"

Axlsx::Package.new do |package|
  service = AxlsxSupportService.new
  styles = service.load_styles(package.workbook)
  sheet_name = "users_" + Time.zone.now.to_date.to_s

  package.workbook.add_worksheet(name: sheet_name) do |sheet|
    sheet.add_row %w(ID メールアドレス ユーザー名), style: styles[:column_title]

    User.find_each do |user|
      sheet.add_row [
        user.id,
        user.email,
        user.name,
      ], style: [
        styles[:fill_zero],
        styles[:default],
        styles[:default],
      ]
    end
  end
end

4. controller から Rubyテンプレートを呼び出す

format ごとに呼び出す機能を変えています。

class UsersController < ApplicationController
  def index
    respond_to do |format|
      # index ページを表示するため html フォーマットについて記述
      format.html do
        @users = User.all
      end
      # index.xlsx.ruby がレンダーされる
      format.xlsx do
        send_data(
          render_to_string.to_stream.read,
          type: :xlsx,
          filename: "users_list_" + Time.zone.now.to_date.to_s + ".xlsx"
        )
      end
    end
  end
end

render_to_string には、先ほど作成した index.xlsx.ruby をレンダリングした結果の文字列が入れられます。

今回の場合は index.xlsx.ruby によって返ってくるのは Axlsx::Package クラスですから、
Axlsx::Package クラスの to_stream メソッドで StringIO クラスに変換したのち読み出し、send_data します。

URLとしては、example.com/users?format=xlsx と打ち込めば、xlsx エクスポート機能が働きます。

5. リンクボタンの設置

view にはこのようなリンクボタンを設置します。
format の指定を入れる点に注意しましょう。

<%= link_to "ユーザーリスト出力", users_path(format: :xlsx) %>

おわりに

急ぎ足でしたが、同じことをやっている人がいなく実装に苦労しましたので、
実装例としてまとめさせていただきました。

情報がなさすぎて、 Axlsx-Rails を使わなければ実装できないのでは……
と挫けそうになったのですが、必要以上の gem 依存におちいることなく実装できて良かったです。

同じことを今後される方のご参考になりますと幸いです。


UUUM では Ruby on Rails が得意な方などなど、挑戦が大好きなエンジニアを募集中です。
お気軽にお問い合わせください。