RailsでAxlsxを使ってxlsxを生成

# 概要
Rails で Exel ファイル (.xlsx) を出力する必要が生じたので、[Axlsx](https://github.com/randym/axlsx) を利用してみた。導入時、ぽっこり空いていた落とし穴に物の見事にハマってしまったが、どうにか抜け出すことができた。無事生還。

# [Axlsx](https://github.com/randym/axlsx)
Axlsx は Ruby 用の Office Open XML Spreadsheet 生成器らしいです。`.xlsx`ファイルを簡単に生成してくれる凄い奴です。

> Axlsx is an Office Open XML Spreadsheet generator for the Ruby programming language.

> [Axlsx Synopsis](https://github.com/randym/axlsx#synopsis)

シノプシスって英単語、英語力低すぎなので初めて見たんですが、要約とか梗概って意味らしいですね。知らなかった。

## Office Open XML
Office Open XML Spreadsheet ってなんやねん。俺は Excel(.xlsx) ファイルが欲しいねん。って方向けの補足です。

> Office Open XML (OpenXML、OOXML) とは、XMLをベースとしたオフィススイート用のファイルフォーマットである。

> [Wikipedia: Office Open XML](http://ja.wikipedia.org/wiki/Office_Open_XML)

Office Open XML は Microsoft Office 2007 以降で採用されているファイルフォーマットで、いわゆる`.docx`とか`.xlsx`とかのことです。つまり、Axlsx が生成してくれる Office Open XML Spreadsheetとは、`.xlsx`ファイルのことです。

詳細は [Wikipedia](http://ja.wikipedia.org/wiki/Office_Open_XML) 大先生が語ってくれてます。

# 導入
## Gemfile
Gemfile に以下を追記し、`bundle install`を実行！

```ruby:Gemfile
gem 'axlsx'
gem 'zip-zip'
```

`zip-zip`という Gem を`axlsx`内部で使っているようなので、一緒に導入します。こいつがないと`cannot load such file -- zip/zip (LoadError)`と、盛大に怒られます。怒られました。

## MIME TYPE
コントローラーで`respond_to`を使って処理したいので、MIME TYPE を Rails に追加します。

`.xlsx`の MIME TYPE は`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`だそうです。とっても長くてびっくり。

下記の通り、`mime_types.rb`に登録しておきます。

```ruby:config/initializers/mime_types.rb
Mime::Type.unregister :xlsx
Mime::Type.register 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', :xlsx
```

`unregister`については、必須ではないと思いますが、行っていない場合下記のような警告が出てしまいました。

```ruby
warning: already initialized constant Mime::XLSX
warning: previous definition of XLSX was here
```

これで、`respond_to`が使えるようになります。

```ruby
def show
  respond_to do |format|
    format.html
    format.xlsx do
       # ここで処理する
    end
  end
end
```

MIME TYPE を追加しなかった場合、`respond_for`を用いると`To respond to a custom format, register it as a MIME type`といった内容のエラーを吐きます。

# 実装
すごく単純に、`show`アクションに`.xlsx`のフォーマットで要求が来たら、Excel ファイルを出力するというプログラムを実装してみました。

呼び出し元の画面には以下のようなリンクを記述しました。

```ruby:show.html.erb
<%= link_to "Excel出力", user_path(@user, format: :xlsx) %>
```

このとき生成されるURLは`/user/id.xlsx`となるため、前述の通り`respond_to`を用いて処理できます。

```ruby:users_controller.rb
def show
  @user = User.find(params[:id])
  
  respond_to do |format|
    format.html
    format.xlsx do
      generate_xlsx # xlsxファイル生成用メソッド
    end
  end
end

private

def generate_xlsx
  Axlsx::Package.new do |p|
    p.workbook.add_worksheet(name: "シート名") do |sheet|      
      sheet.add_row ["First Column", "Second", "Third"]
      sheet.add_row [1, 2, 3]
    end
    send_data(p.to_stream.read,
              type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
              filename: "sample.xlsx")
  end
end
```

これでリンクをクリックで、Excel ファイルを出力するプログラムができました。


## 超簡易サンプル
背景色を変更し、フォントを太字にしてみただけの超しょぼいサンプルです。

```ruby
def generate_xlsx
  Axlsx::Package.new do |p|
    p.workbook.add_worksheet(name: @user.name) do |sheet|
      styles = p.workbook.styles
      title = styles.add_style(bg_color: 'c0c0c0', b: true)
      header = styles.add_style(bg_color: 'e0e0e0', b: true)
      
      sheet.add_row ["ユーザ情報"], style: title
      sheet.add_row %w(名前 年齢), style: header      
      sheet.add_row [user.name, user.age]
    end
    send_data(p.to_stream.read,
              type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
              filename: "#{user.name}.xlsx")
  end
end
```

Excel のレイアウト調整などに関しては、[こちら](http://axlsx.blog.randym.net/)を参考にするのが良いかと思います。

# 落とし穴
`Package#to_stream`を呼び出した時に、``undefined method \`reopen' for "streamed":String``ってエラーが出て爆死しました。

## 原因
エラーメッセージそのままで検索をかけたところ、[undefined method \`reopen' for "streamed":String](https://github.com/straydogstudio/axlsx_rails/issues/18) という、`axlsx_rails`という Gem の Issue がヒットしました。

結論だけまとめると、

1. Axlsx 2.0.0 は Rubyzip 1.0.0 以上では動かない。Rubyzip 0.9.9を使う必要がある。
2. Axlsx 2.0.1 は RubyZip 1.1.1 以上では動かない。RubyZip 1.1.0 以下を使う必要がある。

とのことです。

Rubyzip という Gem も内部では使っていて、こいつのバージョンが想定外のものになっているのが原因のようです。

## 対応策
Gemfile で使用する Rubyzip のバージョンを指定してやれば大丈夫です。特に何かを指定していない場合、最新バージョンの`2.0.1`が入っていると思うので、Rubyzip を以下の通り指定します。

```ruby:Gemfile
gem 'rubyzip', '= 1.1.0'
```

その後、`bundle update rubyzip`を実行で無事解決しました。

# Axlsx-Rails
[Axlsx-Rails](https://github.com/straydogstudio/axlsx_rails) の説明をざっとみたところ、こっちを使ったほうが Rails っぽく綺麗に実装できそうでしたので、利用してみました。

## 概要
Rails ではコントローラーがリクエストを受け取り、それに応じたビューを表示します。例えば、以下のようなコントローラーがあった場合、`html`フォーマットでリクエストを受け取れば、`app/views/users/show.html.erb`が表示されます。

```ruby:users_contoroller.rb
def show
  respond_to do |format|
    format.html
    format.xlsx
  end
end
```

それと同じように、`xlsx`フォーマットで受け取ったら、それに対応したビューを表示しちゃおうぜってのが、Axlsx-Rails です。たぶん。上記の例の場合、`xlsx`のフォーマットでリクエストが来た場合、`app/views/users/show.xlsx.axlsx`を表示してくれます。

## 導入
Gemfileに1行追加で終了です。

```ruby:Gemfile
gem 'axlsx_rails'
```

## 実装
基本的には、先ほどコントローラー内で実装した`Axlsx::Package`に対する処理を`app/views/users/show.xlsx.axlsx`に移管してあげれば終了です。

注意点としては以下の3点かと思います。

1. ファイル名の指定はコントローラーで行う
2. `Axlsx::Package.new`は不要で`xlsx_package`を使用する
3. `send_data`が不要になる

具体的なソースは以下のような感じです。

```ruby:users_controller.rb
def show
  @user = User.find(params[:id])
  
  respond_to do |format|
    format.html
    format.xlsx do
      response.headers['Content-Disposition'] = "attachment; filename='#{@user.name}.xlsx'"
    end
  end
end
```

```ruby:show.xlsx.axlsx
wb = xlsx_package.workbook
wb.add_worksheet(name: @user.name) do |sheet|      
  sheet.add_row ["First Column", "Second", "Third"]
  sheet.add_row [1, 2, 3]
end
```

さきほどよりも Rails っぽい実装になりました。

# 所感
落とし穴にハマった時は絶望したが、とても簡単に Excel ファイルを生成できた。Excel のデザインやらにこだわりだすと大変そうだが、簡単なものでいいならサクッと実装できそう。

Axlsx の最終更新日、2013/09/12(September.12.13) で止まってるけど、大丈夫なんだろうか。

# 参考文献
- [Guides for using axlsx and acts\_as\_xlsx](http://axlsx.blog.randym.net/)
- [axlsx (gem) で xlsx ファイルをダウンロード](http://rails.hatenadiary.jp/entry/2013/02/25/160344)
- [Axlsx-Rails — Axlsx templates for Rails views](http://www.rubydoc.info/gems/axlsx_rails/0.1.4/frames)