DataMapper Reading (その 3: 検索メソッド all の処理)

かなーり間が空いてしまいましたが,続きます.早く version 10.x に突入したい.

今回は代表的な検索用メソッドである Model#all を読んでいきます.

Model#all

module DataMapper
  module Model
    def all(query = {})
      query = scoped_query(query)
      query.repository.read_many(query)
    end

処理としては,

  1. クエリ更新
  2. クエリ実行 (後述するが,実際に 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

が呼び出されたときである.ここで,

  1. scope_stack が返す配列に query を追加
  2. query を引数にしてブロック呼び出し
  3. 終わったら 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 になる).