かなーり間が空いてしまいましたが,続きます.早く version 10.x に突入したい.
今回は代表的な検索用メソッドである Model#all を読んでいきます.
Model#all
module DataMapper module Model def all(query = {}) query = scoped_query(query) query.repository.read_many(query) end
処理としては,
- クエリ更新
- クエリ実行 (後述するが,実際に SQL が発行されるとは限らない)
の流れ.query.repository は Repository インスタンスを返す.Repository インスタンスは Adapter を持っていて,引数として渡されたクエリオブジェクトから実際の SQL を発行する.
では,"query" はいったいどうやって作られるのか?
Model#scoped_query
module DataMapper module Model def scoped_query(query = self.query) assert_kind_of 'query', query, Query, Hash return self.query if query == self.query query = if query.kind_of?(Hash) Query.new(query.has_key?(:repository) ? query.delete(:repository) : self.repository, self, query) else query end if self.query self.query.merge(query) else merge_with_default_scope(query) end end
Model はモデルクラスに extend メソッドで注入 (表現あってるか?) されているため,上記メソッドにおける self はモデルクラスそのものであることに注意.
引数として渡された query が,現在保持する Query インスタンスと等価であればそのまま返す (self.query については後述).ちなみに等価判定 ("==" メソッド) は Query クラスに定義されている.
次に,等価でない場合.
まず,Hash であれば Query インスタンスに変換する.このとき引数として,Hash が :repository を持つ場合はその値を (query.delete の部分),そうでない場合は Model が保持する Repository インスタンスを指定する.残りはモデルクラス自身と引数の query である.
ここまでの処理で,query は Query のインスタンスを指していることになる.
最後に,self.query の状態に応じてマージを行う.
まずは self.query が nil でない場合.
Query#merge メソッドは,自身を dup したあと Query#update を呼び出す.ちなみに,self.query != nil ということは,query != nil である (← all(nil) とでも呼び出さない限りは) ため,Query#merge メソッドの引数も not nil となる.
module DataMapper class Query ... def merge(other) dup.update(other) end ... def update(other) assert_kind_of 'other', other, self.class, Hash assert_valid_other(other) if other.kind_of?(Hash) return self if other.empty? other = self.class.new(@repository, model, other) end return self if self == other # ...フィールドの更新処理... (略) update_conditions(other) self end
まず Query#assert_valid_other メソッドで引数の other をチェックする.other が Hash の場合は何もせずに戻ってくるが,Query インスタンスの場合は other.repository と other.model が self のものと一致するかどうかを確認する.一致していないと ArgumentError になる.
次に,other が空の Hash であれば self を返す.何か入っていれば Query インスタンスに変換する.other が Query インスタンスであることが確定したところで self と比較し,等しければ self を返す.何か違いがあれば other の情報を使って self のフィールドを上書き更新する.
さて,Model#scoped_query へ戻って,else 側を見てみる.
self.query が nil の場合,merge_with_default_scope メソッドにて Query を作成することになる.ちなみにこのメソッドは Scope モジュールで定義されている.Scope に定義されたメソッドは,Model.append_extensions に自身 (Scope) を指定することで,Model の extend 時 ( = include DataMapper::Resource 時) にモデルクラスのクラスメソッドとして追加される (ここら辺は前回の内容を参照).
module DataMapper module Scope Model.append_extensions self
Scope#merge_with_default_scope
基本的には,引数の query から新しく Query インスタンスを生成するだけである.
module DataMapper module Scope def merge_with_default_scope(query) DataMapper::Query.new(query.repository, query.model, default_scope_for_query(query)).update(query) end ... def default_scope_for_query(query) repository_name = query.repository.name default_repository_name = query.model.default_repository_name self.default_scope(default_repository_name).merge(self.default_scope(repository_name)) end ... def default_scope(repository_name = nil) repository_name = self.default_repository_name if repository_name == :default || repository_name.nil? @default_scope ||= {} @default_scope[repository_name] ||= {} end
少し気になるのは default_scope_for_query メソッドを呼び出しているところ.このメソッドは,query の持つ repository.name から特定される Hash と,モデルクラスの持つ default_repository_name から特定される Hash をマージして返す.
この Hash は,Discriminator や Paranoid* 系の property を利用した場合に利用されている.
Repository#read_many
さて,最後は query.repository.read_many(query) の部分.
module DataMapper class Repository ## # retrieve a collection of results of a query # # @param <Query> query composition of the query to perform # @return <DataMapper::Collection> result set of the query # @see DataMapper::Query def read_many(query) adapter.read_many(query) end
実際の処理は Adapter に委譲される.実際の処理を追いかるには DataObjectsAdapter を参照すればよいが,そこから先は長いので,次回に紹介しようと思う.
ざっくりと解説すると,read_many が返すのは
module DataMapper class Collection < LazyArray
のインスタンスであり,これが要である.
Collection クラスは initialize でブロックを引数にとる.Collection インスタンスを実際に作成するのは Adapter であり,Adapter は SQL 発行コードをブロックとして Collection インスタンスに渡している.Collection インスタンスは,kicker となるメソッドが呼び出されない限り,ブロックを call しない.
kicker メソッドは LazyArray に定義されており,Array や Enumerable の public メソッドが kicker として再定義されている.kicker メソッドは,内部で LazyArray#lazy_load メソッドを呼び出す.lazy_load メソッドは Collection に渡されたブロックを call するため,kicker メソッドが呼び出されると SQL が発行される.
メソッドチェインした場合は,
module DataMapper class Collection < LazyArray # ← Model モジュールではない (同じ名前のメソッドがあるけど) ... def all(query = {}) return self if query.kind_of?(Hash) ? query.empty? : query == self.query query = scoped_query(query) query.repository.read_many(query) end ... def scoped_query(query = self.query) ... return self.query if query == self.query query = if query.kind_of?(Hash) Query.new(query.has_key?(:repository) ? query.delete(:repository) : self.repository, model, query) else query end if query.limit || query.offset > 0 set_relative_position(query) end self.query.merge(query) end
のように,自身のクエリに新しいクエリをマージした上で,再度 Repository#read_many を呼び出す.当然この呼び出しも Collection を返すため,kicker メソッドが呼び出されるまで SQL は発行されない.
self.query
さてさて,ずいぶんとほったらかしにしてしまった self.query について.
こいつは Scope モジュールに定義されている.既出の merge_with_default_scope メソッド同様,Model#extended メソッドにてモデルクラスへ注入される.
module DataMapper module Scope ... def query scope_stack.last end ... def scope_stack scope_stack_for = Thread.current[:dm_scope_stack] ||= {} scope_stack_for[self] ||= [] end ...
スレッドローカルにハッシュが入っていて,self をキーに配列を取り出す.query メソッドはその末尾を返している.
query メソッドが nil 以外を返すためには,scope_stack_for[self] の返す配列に,何らかの Query インスタンスが入っていないといけない.
では,どこで要素が挿入されるのかといえば,
module DataMapper module Scope def with_exclusive_scope(query) query = DataMapper::Query.new(repository, self, query) if query.kind_of?(Hash) scope_stack << query begin return yield(query) ensure scope_stack.pop end end
が呼び出されたときである.ここで,
- scope_stack が返す配列に query を追加
- query を引数にしてブロック呼び出し
- 終わったら scope_stack の返す配列から削除
といった流れで使用される.
じゃあ yield には何がくるんだろうか?このメソッドは,Scope#with_scope メソッドから呼び出される.
module DataMapper module Scope ... def with_scope(query) # merge the current scope with the passed in query with_exclusive_scope(self.query ? self.query.merge(query) : query) {|*block_args| yield(*block_args) } end
なんと,また yield.
では,どこで使われているのかといえば,
module DataMapper class Collection ... def method_missing(method, *args, &block) if model.public_methods(false).map { |m| m.to_s }.include?(method.to_s) model.send(:with_scope, query) do # ←ここね model.send(method, *args, &block) end elsif relationship = relationships[method]
のように,Collection インスタンス =「検索系メソッドの戻り値」をレシーバにしてメソッドを呼び出した時,そのメソッドが「Collection には定義されていないが,model には定義されている」場合,Scope#with_scope メソッドが呼び出される.
この時のブロック内部では,model をレシーバとして method を呼び出す.model は Collection クラスに定義された accessor であり,Collection インスタンスが保持する query インスタンスから取得したモデルクラスを返す.
なぜこんなことをしているのかと言えば,ユーザ定義の検索系メソッドをメソッドチェインして呼び出したときに対応するためである (絞り込み操作に該当).
class Post ... def self.published all(:published => true) end end posts = Post.all(:author => 'krdlab') posts = posts.published # query のマージと with_scope 呼び出しが発生
まとめ
Model#all Model#scoped_query # マージしたクエリを返す Repository#read_many # Collection オブジェクトを返す
Collection オブジェクトは,kicker メソッドが呼び出されない限りクエリ (SQL) を発行しない.
kicker メソッドは,内部で LazyArray#lazy_load を呼び出すメソッドである (Array,Enumerable の public メソッドはほとんど kicker になる).