taslamの日記

>>mizincogrammerに移転しました。こちらは、今後更新されません。<<

2008-05-28

[][]ActiveResourceが遅い→JSONならパースが速いよ

きっかけ

ネットワーク越しだし、速度が出ないのはまぁいいんだけど、それにしたって遅い。

具体的にはXMLパースが遅い、遅すぎる。

なんとかならぬか。

どうやらXMLSimpleがボトルネックらしい。

JSON使った方がましかなぁ。

パーサの速度比較

同じデータをto_jsonとto_xmlでそれぞれシリアライズしたファイルを用意。(20数個のフィールドを持つレコード20件のもの。)

Hash.from_xml(XmlSimple)、ActiveSupport::JSON.decode、JSON.parse(JSON implementation for Ruby)それぞれでパースに必要な時間を測定してみた。

Benchmark.bm do |x|
  x.report { 10.times{ Hash.from_xml(xml) } }
  x.report { 10.times{ ActiveSupport::JSON.decode(json) } }
  x.report { 10.times{ JSON.parse(json) } }
end
      user     system      total        real
  0.870000   0.530000   1.400000 (  1.394237)
  0.160000   0.010000   0.170000 (  0.168284)
  0.000000   0.000000   0.000000 (  0.006735)

JSON.parse、圧倒的じゃないか・・・

   / ̄ ̄\
 /   _ノ  \
 |    ( ●)(●)
. |     (__人__)   XmlSimple遅すぎだろ
  |     ` ⌒´ノ    常識的に考えて…
.  |         }
.  ヽ        }
   ヽ     ノ        \
   /    く  \        \
   |     \   \         \
    |    |ヽ、二⌒)、          \

経過

コントローラ
def index
  #  省略
  respond_to do |format|
    format.xml { render :xml => @shops }
    format.xml { render :xml => @shops }
  end
end

def create
  #  省略
  respond_to do |format|
    if @shop.save
      format.xml { render :xml => @shop, :status => :created, :location => @shop }
      format.json { render :json => @shop, :status => :created, :location => @shop }
    else
      # ActiveRecord::Errors#to_xmlは専用のものでオーバーライドされているが、
      #to_jsonはそうでは無く、TypeError: wrong argument type Hash (expected Data)が発生してしまうのに注意。
      format.xml { render :xml => @shop.errors, :status => :unprocessable_entity }
      format.json { render :json => { :errors => @shop.errors.full_messages }, :status => :unprocessable_entity }
    end
  end
  
  # show/update/destroyも同様に。
end
ActiveResource

ActiveResource::Base.formatを変更する。

class Shop << ActiveResource::Base
  self.site = 'http://taslam-example.jp/'
  self.format = :json  # ActiveResource::Formats:JsonFormatを使う
end

しかし、、

Shop.find(:all).first.name # => "\\u30a2\\u30c9\\u30c7\\u30b6\\u30a4\\u30f3"

日本語がうまくデコードされないみたいだ。

ActiveResource::Formats:JsonFormatを見てみると、

def decode(json)
  ActiveSupport::JSON.decode(json)
end

どうやら、ActiveSupport::JSON.decodeがだめみたい。

試してみると、JSON.parseだとうまくデコードされる模様。

そこで、このActiveSupport::JSONJSONに置き換えたActiveResource::Formats:ExJsonFormatを作って、こちらを使うようにする。こっちのが速いしね。

# application.rbの末尾などに
require 'json'
module ActiveResource
  module Formats
    module ExJsonFormat
      include ActiveResource::Formats::JsonFormat  

      def decode(json)
        JSON.parse(json)
      end
      
      extend self
    end
  end
end
class Shop << ActiveResource::Base
  self.site = 'http://taslam-example.jp/'
  self.format = :ex_json
end

これで大丈夫かと思ったんだけど、テストしてみるとリソースの作成、更新に失敗しているみたい。ActiveRecord::Baseを見てみる。

def to_xml(options={})
  attributes.to_xml({:root => self.class.element_name}.merge(options))
end

def create
  returning connection.post(collection_path, to_xml, self.class.headers) do |response|
    self.id = id_from_response(response)
    load_attributes_from_response(response)
  end
end

データをXMLで送ってるのかな。ActiveResource::Connection#postを追ってみる。

def post(path, body = '', headers = {})
  request(:post, path, body.to_s, build_request_headers(headers))
end

def default_header
  @default_header ||= { 'Content-Type' => format.mime_type }
end

def build_request_headers(headers)
  authorization_header.update(default_header).update(headers)
end

Content-type: application/json で、 /shops.json に、データをXMLで送ってたみたいだ。受け取ってる側のログをみると、

Processing ShopsController#create (for 192.168.1.9 at 2008-05-29 15:01:15) [POST]
  Parameters: {"format"=>"json", "action"=>"create", "controller"=>"shops"}
Completed in 0.38161 (2 reqs/sec) | Rendering: 0.00029 (0%) | DB: 0.00000 (0%) | 422 Unprocessable Entity [http://taslam-example.jp/shops.json]

やっぱり、データが受け取れてないや。

ひとまず、検索時だけJSONで高速化できれば当初の目的は達成できる(し、JSONに変更したことでもし不具合がでてもデータの更新ができないなどということにならない)ので、findを使う際のみformatを切り替えることにした。

class Shop << ActiveResource::Base
  self.site = 'http://taslam-example.jp/'
  
  def self.find(*args)
    self.format = self.connection.format = ActiveResource::Formats[:ex_json]
    super(*args)
  ensure
    self.format = self.connection.format = ActiveResource::Formats[:xml]
  end

end

2008-05-02

[]使ってみたけど

良いと思った点

不便だなぁと思った点

  • ページネーション。limitとoffsetで検索範囲を指定できるように実装しても、ページネーションに関する情報は付加できないので、どうしてくれよう。最大件数をfindなんかで取得したりしたら大変なことになるし。
  • 検索については使い方にあわせて専用のAPIを設計した方が効率的かも。

2008-03-24

[][]ActiveResourceを試すための準備。

RESTって?

http://yohei-y.blogspot.com/2005/04/rest_23.html:REST入門]

ActiveResourceって?

RESTアーキテクチャでいうところのリソースActiveRecordと同様のインタフェースで扱うためのマッパ。

TwitterAPIをActiveResourceで

TwitterのAPIでRailsのActive Resouceを試してみる

リソース

ActiveResourceに対応したコントローラを作成するための規約を知る

基本的なこと。

WSSE認証

あまり良いサンプルじゃないけど。

[][]Basic認証以外に対応させる

HTTPリクエストヘッダに認証情報を付加する場合(WSSEとか)。

ソースを追ってみるとActiveResource::Connection#authorization_headerをオーバーライドしてやれば良い様だ。

# Sets authorization header; authentication information is pulled from credentials provided with site URI.                 
def authorization_header
  (@site.user || @site.password ? { 'Authorization' => 'Basic ' + ["#{@site.user}:#{@site.password}"].pack('m').delete("\r\n") } : {})
end

ActiveResource::Connectionのインスタンスは、ActiveResource::Base.connectionで生成されているので、ここで、Connectionの代わりに継承してつくったクラスを指定してやれば良いだろう。

[][][]ActiveResourceでWSSE認証

Basic認証以外に対応させるの例として、WSSE認証。

require 'digest/sha1'
require 'base64'
  
class ActiveResource::Connection::WSSE < ActiveResource::Connection
  private
  def authorization_header
    if @site.user || @site.password
      {
        'Accept' => 'application/x.atom+xml, application/xml, text/xml, */*',
        'X-WSSE' => credentials 
      }
    else
      { }
    end
  end
  
  private
  def created
    Time.now.utc.iso8601
  end
  
  def nonce
    @nonce ||= Digest::SHA1.digest(Digest::SHA1.digest(Time.now.to_s + sprintf("%f", rand()) + $$.to_s))
  end
     
  def digest
    nonce1 = nonce
    Digest::SHA1.digest(nonce1 + created + @site.password)
  end
  
  def credentials
    sprintf(%(UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"),
            @site.user, Base64.encode64(digest).chomp, Base64.encode64(nonce).chomp, created)
  end

end


class Entry < ActiveResource::Base

  USER = 'taslam'
  PASSWORD = 'password'
  self.site = "http://#{USER}:#{PASSWORD}@hoge.com"
  self.logger = Logger.new($stderr)
     
  def self.connection(refresh = false)
    @connection = ActiveResource::Connection::WSSE.new(site, format) if refresh || @connection.nil?
    @connection
  end

end