DataMapper Reading (その 2: モデルの save 処理)
今回はモデルの save 処理を軸に読み進めていきます.
Resource#save メソッド
モデルの save メソッドは,Resource モジュールに定義されている.
module Resource def save(context = :default) ... associations_saved = false child_associations.each { |a| associations_saved |= a.save } saved = new_record? ? create : update if saved original_values.clear end parent_associations.each { |a| associations_saved |= a.save } # We should return true if the model (or any of its associations) # were saved. (saved | associations_saved) == true end
引数の context は,dm-validations を使用する場合に意味が出てくる.
save メソッドのうち,今回注目すべきは以下の部分.
saved = new_record? ? create : update
Resource#new_record? は,@new_record が定義されていなければ true を,それ以外の場合は @new_record の値を返す.そして @new_record は,Resource#create を呼び出して成功すると false が,Resource#destroy を呼び出すと true が設定される.
つまり,new したばかりのモデルインスタンスは #new_recourd? == true となる.
Resource#create メソッド
このメソッドをみると,Repository,Adapter,Resource (モデル) の関係がよくわかる (と思うんだけど...).
module DataMapper module Resource def create return false if new_record? && !dirty? && !model.key.any? { |p| p.serial? } # set defaults for new resource properties.each do |property| next if attribute_loaded?(property.name) property.set(self, property.default_for(self)) end return false unless repository.create([ self ]) == 1 @repository = repository @new_record = false repository.identity_map(model).set(key, self) true end
まず 1 行目で,新しいレコードなのに,dirty? == false つまり値が変更されていなくて,かつ Serial な property が定義されていない場合は,false を返す.つまり保存失敗.
次に,モデルが保持するプロパティをチェックし,プロパティがロード済みでなければデフォルト値を設定していく.ロード済みかどうかは Resource#attribute_loaded? が判断する.
module DataMapper module Resource def attribute_loaded?(name) instance_variable_defined?(properties[name].instance_variable_name) end
判断基準は「プロパティに対応するインスタンス変数が定義されているかどうか」である.ところで,instance_variable_defined? というメソッドは Ruby 標準ではない.これは extlib/object.rb で,Object class に追加されたメソッドである.
class Object unless respond_to?(:instance_variable_defined?) def instance_variable_defined?(variable) instance_variables.include?(variable.to_s) end end
見てもらえば一発だが,指定した変数がインスタンス変数に含まれているかどうかを調べているだけ.save メソッドを使う場合,事前にプロパティを代入する (つまり,インスタンス変数が定義される) ため,代入済みのプロパティについてはロード済みとみなされる.
それ以外のプロパティに対しては,デフォルト値を設定する.そのデフォルト値は Property#default_for が生成する.
module DataMapper class Property def default_for(resource) @default.respond_to?(:call) ? @default.call(resource, self) : @default end
モデルクラスを定義するとき,各プロパティには :default を指定することができる.プロパティに :default で Proc を指定した場合,上のコードでは @default.respond_to?(:call) == true なので @default.call の戻り値を返す.値を指定した場合は,その値がデフォルト値として使用される.何も指定していなければ nil となる.
以上でプロパティの初期化は完了.
さて,ついに本丸 Repository インスタンスの登場.
return false unless repository.create([ self ]) == 1
Resource#repository メソッドは以下の通り.
module DataMapper module Resource def repository @repository || model.repository end
初めて create メソッドが呼ばれた場合は @repository が設定されていないため,model.repository へと流れる.
ここで出てきた model は class の alias.
module DataMapper module Resource alias model class
つまり Resource#repository は,インスタンス変数 @repository が
- nil ではない場合,その値をそのまま返す
- nil の場合,クラスメソッドの repository メソッドを呼び出し,その戻り値を返す
では,後者の repository メソッドはどこで定義されているかというと,
module DataMapper module Resource ... def self.included(model) model.extend Model # ← これ ...
上記の箇所でモデルクラスを extend している Model モジュールに定義されている.Object#extend は,引数のモジュールで定義されているメソッドを,レシーバの特異メソッドとして追加する.つまり上記の場合はモデルクラスのクラスメソッドとして追加されることになる (ここら辺は「プログラミング Ruby 第 2 版 言語編」の第 24 章あたりが詳しい).
実際の Model#repository メソッドは以下の通り.
module DataMapper module Model def repository(name = nil) if block_given? DataMapper.repository(name || repository_name) { |*block_args| yield(*block_args) } else DataMapper.repository(name || repository_name) end end
ここで既出の DataMapper.repository メソッドとつながった.今回の Resource#repository からの呼び出しは引数なしであるため,DataMapper.repository(repository_name) として呼び出される.そして Model#repository_name は以下の通り.
module DataMapper module Model def repository_name Repository.context.any? ? Repository.context.last.name : default_repository_name end
Repository.context に Repository インスタンスが登録されていれば,その中の最後の要素から name が,Repository インスタンスが登録されていなければ default_repository_name が使用される (Model#default_repository_name は Repository.default_name を返す,Repository.default_name は :default を返す).前回述べたが,DataMapper.repository メソッドにブロックを渡して呼び出すか,Repository#scope を使わない限り,Repository.context.any? が true となることはない (Repository#scope メソッドを参照).つまりは以下のようにしない限り,必ず :default のコンテキストで実行される,ということになる.
h = Hoge.new # Hoge が DataMapper のモデルクラスだとして ... DataMapper.repository(:hoge) { h.save } # もしくは class Hoge include DataMapper::Resource ... def self.default_repository_name :hoge end end
なお,一度 Repository インスタンスが確定すると (モデルインスタンスの @repository に代入されると),以降の操作はすべて @repository 経由となる.今回の save 処理でいえば,save の成功したモデルインスタンスは,以後 save 時の Repository インスタンス上で操作が実行されることになる.
さてさて,これで Repository インスタンスが手に入った.で,戻ってくる場所はここである.
return false unless repository.create([ self ]) == 1
Repository#create メソッドへと進む.
module DataMapper module Repository def create(resources) adapter.create(resources) end
続いて Repository#adapter は,
module DataMapper module Repository def adapter @adapter ||= begin raise ArgumentError, "Adapter not set: #{@name}. Did you forget to setup?" \ unless self.class.adapters.has_key?(@name) self.class.adapters[@name] end end
ここでまた既出の DataMapper.setup メソッドとつながる.ここで取り出している self.class.adapters[@name] は,
module DataMapper def self.setup(name, uri_or_options) ... Repository.adapters[name] = Adapters::const_get(class_name).new(name, uri_or_options) end
で登録した Adapter インスタンスである.Adapter インスタンスは,AbstractAdapter クラスから派生したクラスのインスタンスである.AbstractAdapter は,各接続先用に実装された Adapter クラスのインタフェースを規定する.また,Adapter は接続先 DB の情報を保持している.モデルが実際にどうクエリへと変換されるかは,各 Adapter の実装に拠る.
これで repository.create([ self ]) は無事に接続先用の Adapter へと到達し,クエリを発行する.
Identity Map
TODO あとで書く
module DataMapper module Resource def key key_properties.map do |property| original_values[property.name] || property.get!(self) end end
Resource#original_values は,save に成功すると clear される.
まとめ
モデルの save 処理の流れは,以下のようになる.
Resource#save Resource#create Resource#properties Model#repository DataMapper#repository # Repository インスタンスを返す Repository#create AbstractAdapter#create # 実際には MysqlAdapter とか Repository#identity_map
Repository は,DataMapper.setup の第一引数で指定した Symbol と結びついている.Repository が接続先 DB 用の Adapter と Identity Map を管理し,Adapter がモデルとクエリの変換処理を行う.