PCがあれば何でもできる!

へっぽこアラサープログラマーが、覚えたての知識を得意げにお届けします

【Rails】AWS:DynamoDBのORM「Dynamoid」の不便な点とその改善方法(その2)

2回目の「Dynamoid」の不便な点と改善方法についてのお話です!

前回の記事はこちら。
【Rails】AWS:DynamoDBのORM「Dynamoid」の不便な点とその改善方法(その1) - PCがあれば何でもできる!

Modelに定義していない属性が、検索レコード内に存在すると落ちる

例えばこんなModelを作ったとして

class User
  include Dynamoid::Document
 
  table :key => :id
  field :name, :string
  field :age, :integer
end

 

こんな感じで検索したとします。

result = User.find(1)

 

そして、検索したレコードがこんな感じになっていると

id   name    age sex
--------------------------------
1   doizaki 29  male

 

Modelにsexのfieldを定義していないため、”sex=”のメソッドが無いと言って落ちます。

(503.68 ms) GET ITEM - [["User", 1, {}]]
NoMethodError: undefined method `sex=' for #<User:0x000001065e4370>

 

この動きのせいで、過去にゴミ属性を作っていると引っかかります。 Dynamoidさん、そこは無視してください orz

対処方法

ということで、コードでDynamoidさんを説得してみたいと思います。

まず、エラーが発生している場所を調べます。

# encoding: utf-8
module Dynamoid #:nodoc:
 
  # This is the base module for all domain objects that need to be persisted to
  # the database as documents.
  module Document
    ...
    module ClassMethods
      ...
      def load(attrs)
        self.class.undump(attrs).each {|key, value| send "#{key}=", value }
      end
      ...
    end
    ...
  end
end

self.class.undumpで取得したレコードの属性をハッシュ化し、#{key}=メソッドでModelの各fieldに値を退避させています。 が、Modelにfieldを定義しないと、このメソッドが作られないため、落ちてしまうようです。

そこで、メソッド実行前に、respond_to?を使って、メソッドの存在チェックを挟みます。書き換えは、Rubyのオープンクラスの特性を活かして、外部から書き換えます。

module Dynamoid
  module Document
    def load(attrs)
      self.class.undump(attrs).each do |key, value|
        send("#{key}=", value) if respond_to?(key)
      end
    end
  end
end

 

これで無事に取得出来ました!

result = User.find(1)
(524.08 ms) GET ITEM - [["User", 1, {}]]
 => #<User:0x00000106674100 @new_record=false, @attributes={:created_at=>Sat, 17 May 2014 17:36:40 +0900, :updated_at=>Thu, 22 May 2014 13:26:47 +0900, :id=>1, :name=>"doizaki", :age=>29}, @associations={}, @changed_attributes={}>

 

さらにパフォーマンスを追求

さて、動きはしたものの、上記の修正については、ややパフォーマンスが心配です。

respond_to?はクラス内(継承、特異含む)のメソッドを全て探索するため、軽い処理とは言いがたいです。

さらに、今回書き換えたloadメソッドは、検索結果のレコード数回実行されるため、respond_to?が実行されるトータル回数は、検索行数☓属性数回となり、かなりの負荷が予想されます。

そこで、改良パターンを2つ考えてみました。

改良パターン

ハッシュを利用するパターン

Dynamoidは、fieldを定義した際に、自動でModelクラスのattributesというハッシュに、属性情報が保存されます。 そのハッシュを利用することで、respond_to?での余計なメソッド探索を防ぎます。

module Dynamoid
  module Document
    def load(attrs)
      self.class.undump(attrs).each do |key, value|
        send("#{key}=", value) if self.class.attributes.include?(key)
      end
    end
  end
end
begin-rescueによる例外処理で回避するパターン

レコード毎、属性毎に、探索を行わないため、ゴミデータが限りなく少ない場合は早いはず。

module Dynamoid
  module Document
    def load(attrs)
      self.class.undump(attrs).each do |key, value|
        begin
          send("#{key}=", value) if self.class.attributes.include?(key)
        rescue (NoMethodError)
        end
      end
    end
  end
end

パフォーマンス検証

ゴミデータが少ない場合

条件

  • レコード: 10万
  • フィールド(属性): 40
  • ゴミデータ: 全レコードに1属性ずつ

結果(3回の平均)

  • respond_to?: 12,943 ms
  • ハッシュ: 11,812 ms
  • begin-rescure: 11,794 ms
ゴミデータが多い場合

条件

  • レコード: 10万
  • フィールド(属性): 30
  • ゴミデータ: 全レコードに10属性ずつ

結果(3回の平均)

  • respond_to?: 11,691 ms
  • ハッシュ: 9,120 ms
  • begin-rescure: 15,786 ms

 

安定して早いのは、ハッシュでした。例外処理の重さをあらためて感じます。

ただし、ゴミデータ入りの行が限りなく少ない場合は、順位が入れ替わるかもしれませんね。