【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
安定して早いのは、ハッシュでした。例外処理の重さをあらためて感じます。
ただし、ゴミデータ入りの行が限りなく少ない場合は、順位が入れ替わるかもしれませんね。