読者です 読者をやめる 読者になる 読者になる

odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

rails3.1で大きいデータ(csv等)をストリーミングで出力する方法

はじめに

管理画面等で膨大なデータをcsvで落としたい時等に
そのままDBから文字列を作って返すと反応がないままタイムアウトになると思います。(なんせ全部メモリに落とすのだから)


こういう場合の回避方法(2パターン)

  1. db等にリクエストをキューイングしておきバッチで処理してzipに圧縮しておく。
    • サーバーにはお行儀がいいお作法。ただし工数がかかる。
  2. 1000件等の細かい単位にしてsqlを発行してストリーミングで出力する。常にブラウザにデータが流れるためタイムアウトしない。
    • 工数少なめ。ただしサーバーに通信しっぱなしになるのでサーバーの同時リクエスト数を食いつぶす。管理画面等の一部利用者のみなら問題ないでしょう。

今回2の方法をrails3.1で行う文献が見つからなかったので調べました。

サンプル

1秒おきにtest1,test2..test5のように出力されます。

class TestController < ApplicationController
  def index

    headers["Cache-Control"] ||= "no-cache"
    headers["Transfer-Encoding"] = "chunked"

    # 上記のheadersを書くかもしくは下記のrenderでもOK。headersのほうが余計な処理が入らないのでお勧めかな??
    #render :text=>"", :stream => true

    # DLにする場合はここのコメントを外せばOK.ブラウザで順次出力されるのを確認できるようにあえて外しておきます。
    # headers.merge!(
      # 'Content-Type' => "text/csv; charset=Shift_JIS",
      # 'Content-Disposition' => "attachment; filename=\"test.csv\""
    # )

    self.response_body = Rack::Chunked::Body.new(Enumerator.new do |y|
      5.times do |i|
        y << "test#{i} \n"
        sleep 1
      end
    end)

  end
end

大抵はDBから1000件ずつ等ちょこちょこ取得して出力するでしょう。その際はfind_eachを使うと便利です。
http://wiki.usagee.co.jp/ruby/rails/RailsGuides%E3%82%92%E3%82%86%E3%81%A3%E3%81%8F%E3%82%8A%E5%92%8C%E8%A8%B3%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F%E3%82%88/Active%20Record%20Query%20Interface#i803e697

注意点

webrickではストリーミングになりません。
unicornを使ってください。
また古いバージョンのunicornだと設定に下記を指定しないといけないそうです。(現在はデフォでこの設定になってる)

# unicorn.config.rb
listen 3000, :tcp_nopush => false

参考

rails3.1の新機能のhttpストリーミング部分のソースを読みました。(実際はrailsのbody部分の出力の流れまで追ってしまいました。。unicornのc部分まで行きそうな所で力尽きましたw)
actionpack-3.1.3/lib/action_controller/metal/streaming.rb

実行環境

ruby (1.9.2p290)
rails (3.1.3)
unicorn (4.1.1)