RailsでAmazonの商品を扱う

Amazonの商品はAPIを介して簡単に取得できますが, 取得した情報はキャッシュ目的以外でローカルに格納してはいけない, などの制約がある. なのでどんな風に書けばいいかメモ. あと毎回APIを介していては遅くなるのでmemcachedを使う(キャッシュ目的なのでOKなはず).

まずAmazonAPIに簡潔にアクセスできるRubyGemsを入れる.

$ sudo gem install amazon-ecs

次にRailsのconfigの中身を書き換えておく.

# config/environment.rb
config.gem 'amazon-ecs', :lib => 'amazon/ecs'
# config/initializers/amazon_ecs.rb
Amazon::Ecs.options = {
  :aWS_access_key_id => 'APIを使うために必要なアクセスキー',
  :country           => :jp,
  :response_group    => 'Medium'
}

そしてマイグレーションファイル.

class CreateAmazonItems < ActiveRecord::Migration
  def self.up
    create_table :amazon_items do |t|
      t.string :asin, :null => false
      t.timestamps
    end
  end

  def self.down
    drop_table :amazon_items
  end
end

ASINのみは恒久的に保持しておいていいのでこうする(他のを含めると規約違反なはず).

後はモデル.

class AmazonItem < ActiveRecord::Base
  validates_presence_of :asin

  def get(path)
    lookup unless @looked
    @item && @item.get(path)
  end

  protected

  def validate
    lookup unless @looked
    errors.add :asin, "can't find the item from Amazon" unless @item
  end

  private

  def lookup
    @item = Amazon::Ecs.item_lookup(asin).first_item
    @looked = true
  end
end

必要なときだけlookupメソッドでAmazonに要求するようにし, 2回目以降は当然リクエストしないようにしておく.

コレで:

amazon_item = AmazonItem.find_by_asin '4797336617'
puts amazon_item.get 'itemattributes/title' # => たのしいRuby 第2版 Rubyではじめる気軽なプログラミング

こんな感じに取得できる.

だけどASINが同一でも, インスタンスが違えば@lookedインスタンス変数は偽となる(nil)なので, そういう場合はやっぱり速度が落ちる. というコトでキャッシュを導入.

$ sudo port install memcached
$ sudo gem install memcache-client
class AmazonItem < ActiveRecord::Base
  CACHE_SERVERS = 'localhost:11211'

  validates_presence_of :asin

  def get(path)
    lookup unless @looked
    @item && @item.get(path)
  end

  protected

  def validate
    lookup unless @looked
    errors.add :asin, "can't find the item from Amazon" unless @item
  end

  private

  def lookup
    unless @@cache
      require 'memcache' # Amazon::Ecsはrequire 'amazon/ecs'しなくても使える(それはそれで怪しい)が……
      @@cache = MemCache.new CACHE_SERVERS
    end
    if @item = @@cache[asin]
      @item = Amazon::Element.new Hpricot @item
    elsif @item = Amazon::Ecs.item_lookup(asin).first_item
      @@cache.add asin, @item.to_s, 24 * 60 * 60
    end
    @looked = true
  end
end

あとconfig/environment.rbにも追記する.

config.gem 'memcache-client', :lib => 'memcache'

コレだけで劇的に早くなった. 発想は単純だけど, 単純だからこそなのか, キャッシュはかなり効果的だなぁ. あとAPIを介して取得した情報はキャッシュしてもいいが, 1日だけという期限がついている. それもmemcachedを使えば, うまい具合にできてしまう. コレはいいや.

気になるところ

  • MacPortsで入れたmemcachedが1.2.8と古い(今は1.4とか?). やっぱりソースから入れた方がいいかな
  • 上記ではAmazonItemクラスひとつにつきmemcachedのクライアントを作っているが, そのスコープは適切なのか. ローカル変数に入れて, その場その場でok? それとももっと大きなスコープでもok?
  • serialize/desirializeが強引. addや[]はMarshal.dump/loadを行うが, uninitialized structと例外が発生してしまうし……