DataMapper 0.10.2 Reading (その 2: dm-core.rb)

やはり,最初はここから.
0.9.x に比べて,格段にモジュール化が進み,非常にシンプルになっている.

最初の方

ひたすら require して,必要なコードを読み込んでいる.所々 TODO があるので,また変わるんだろうなぁ...

DataMapper.root

module DataMapper
  def self.root
    @root ||= Pathname(__FILE__).dirname.parent.expand_path.freeze
  end

dm-core パッケージのディレクトリパスを設定する.確かに freeze しておくのは手だなぁ.

DataMapper.setup

なんかシンプルになってるし...

module DataMapper
  def self.setup(*args)
    adapter = args.first

    unless adapter.kind_of?(Adapters::AbstractAdapter)
      adapter = Adapters.new(*args)
    end

    Repository.adapters[adapter.name] = adapter
  end

args は可変長引数なので Array インスタンスである.setup メソッドの呼び出し方が

DataMapper.setup(:default, 'mysql://localhost/dm_test')
DataMapper.setup(:default, {
  :adapter  => 'postgres',
  :database => 'localhost/dm_test',
  ...
})

であることから,ローカル変数 adapter はコンテキスト (Repository) を識別するための Symbol が入る.adapter が AbstractAdapter 派生のインスタンスでなければ (←通常は Symbol なのでこのパターン),引数 args から新しい Adapter を作成する (MysqlAdapter 等).Adapters.new メソッド内部では,以前紹介した DataMapper.setup メソッドのように uri/Hash から Adapter インスタンスを作成しているが,このコードがまたきれいにまとめられていて,とても読みやすい (Adapters についてはまた今度).

最後は name を key として Adapter インスタンスが Repository.adapters に蓄えられる.adapter.name は,setup メソッドの第 1 引数に渡した Symbol である.

ところで,adapter.kind_of? で AbstractAdapter かどうかを見ているということは,setup メソッドの引数に Adapter インスタンスが渡せるということか?

DataMapper.repository

このメソッドはあまり変わっていない.ただ,コードが整理され,すっきりしてる.

module DataMapper
  def self.repository(name = nil)
    context = Repository.context

    current_repository = if name
      assert_kind_of 'name', name, Symbol
      context.detect { |repository| repository.name == name }
    else
      name = Repository.default_name
      context.last
    end

    current_repository ||= Repository.new(name)

    if block_given?
      current_repository.scope { |*block_args| yield(*block_args) }
    else
      current_repository
    end
  end

基本的な流れは以下の通り.

  1. context (コンテキストスタック) を取得
  2. 引数に name が指定されていれば,context から名前の一致する Repository インスタンスを取得
  3. 引数に name が指定されていなければ,:default を name とし,context から末尾の Repository インスタンスを取得
  4. context から Repository インスタンスがとれなかった (nil だった) 場合は,新しい Repository インスタンスを生成
  5. block が渡されていれば,block 渡して Repository#scope を呼び出す
  6. block が渡されていなければ,そのまま Repository インスタンスを返す

block 有りの呼び出しは,以下のようにコンテキストを明示して呼び出す際に用いる.

p = DataMapper.repository(:hoge) { Person.first }

block 無しの呼び出しは,主にモデルメソッドの内部におけるコンテキスト取得に用いられる.


Repository.context メソッドは,スレッドローカルから Repository インスタンスの配列 (コンテキストスタック) を取り出す.

module DataMapper
  class Repository
    def self.context
      Thread.current[:dm_repository_contexts] ||= []
    end

Repository#scope メソッドは,Repository.context の返すコンテキストスタックに self を積み,渡された block を実行する.

module DataMapper
  class Repository
    def scope
      context = Repository.context

      context << self

      begin
        yield self
      ensure
        context.pop
      end
    end

これは,yield で実行されるブロックの内部において,モデルメソッドの呼び出しで context 取得が行われた際に,正しい context を返すための処置である.

モジュール/クラス間の関係

無理矢理書いたのでおかしいかも.

DataMapper 0.10.2 Reading (その 1: パッケージ)

今回から 0.10.2 のコードへ突入.
その前に,0.9.11 のコードを読み進めたときの反省点.

  1. 全体像の把握をきちんとしていなかった
    • 気づいたら深さ優先になってた
  2. 図がない!
    • 自分で読み返してもわかりづらい!

と,いうわけで,気をつけろよ!>自分

パッケージ

  • dm-core
    • コアパッケージ.DataMapper の基本的な機能が含まれる.DataMapper::Resource とかはここに入ってる.
  • do
    • Database Driver 集.DataObjects モジュールとして interface が定義されている.do_mysql や do_sqlite3 等はここに含まれている.
  • dm-more
    • DataMapper の拡張機能とか,その他いろいろ含まれている.dm-aggregates とか dm-validations はここに入ってる.Rails 上で DataMapper 使うためのパッケージもここ.あ,なんか "dm-rest-adapter" ってのもあるみたい.(以下引用)
Extra, non-essential parts for DataMapper
Including
  * Migrations
  * Validations
  * Serializers
  * A Command-Line Interface
  * Timestamps
  * Types
  * ActiveRecord-style finders
  * Adapters for your favorite repository or database type
  * Integration with your favorite web frameworks
  * Observers for watching resources.
  • extlib
    • サポートライブラリ.主に ruby 標準クラスの拡張が含まれている.lazy_array とかはここに入ってる.
  • data_mapper
    • よく使われるパッケージを一括して require.
      • dm-core
      • dm-aggregates
      • dm-constraints
      • dm-migrations
      • dm-serializer
      • dm-timestamps
      • dm-validations
      • dm-types

名前データが大量に欲しくなって

テーブルに突っ込むデータとして,名前データが欲しくなったわけです.そしたらこんな記事を発見.
http://itpro.nikkeibp.co.jp/article/COLUMN/20060131/228230/


とりあえず ruby 用に書き直してみた.重複云々の部分 (元記事の if 文) は省略しています.

NE = [
  ["","","","","","","","","","","","","",""],
  ["","","","","","","","","","","","",""],
  ["","","","","","","","","","","","",""],
  ["","","","","","一郎","","","","","太郎",""]
]
NUM = 1000
sel = lambda {|a| a[rand(a.size)] }
names = (0...NUM).inject([]) do |a,i|
  a << (0...NE.size).inject("") {|s,j| s << sel[NE[j]] }
end
open('output.txt', 'w') do |f|
  f.puts names
end

出力してみると,以外とそれっぽい名前になってる.

DataMapper Reading (その 4: Adapter 補足)

前回積み残した TODO の Adapter について.

実際に DB へ SQL 発行を行うのが Adapter なので,こいつを実装すればいろんなストレージにアクセスできるようになる.なお,自分で Adapter を実装する場合は,in_memory_adapter.rb が参考になる.

AbstractAdapter#read_many

module DataMapper
  module Adapters
    class AbstractAdapter
      def read_many(query)
        raise NotImplementedError
      end

何もないっす.

DataObjectsAdapter#read_many

ここがメイン.個々のストレージに依存しないコードになっている.

module DataMapper
  module Adapters
    class DataObjectsAdapter < AbstractAdapter
       def read_many(query)
        Collection.new(query) do |collection|
          with_connection do |connection|
            command = connection.create_command(read_statement(query))
            command.set_types(query.fields.map { |p| p.primitive })

            begin
              bind_values = query.bind_values.map do |v|
                v == [] ? [nil] : v
              end
              reader = command.execute_reader(*bind_values)

              while(reader.next!)
                collection.load(reader.values)
              end
            ensure
              reader.close if reader
            end
          end
        end
      end

メインの処理は with_connection に渡しているブロックの部分.ちなみに with_connection メソッドは,コネクションの生成→利用→クローズといった一連の流れと,エラー時のロギング処理が入ったベーシックなメソッドである.

module DataMapper
  module Adapters
    class DataObjectsAdapter < AbstractAdapter
      def with_connection
        connection = nil
        begin
          connection = create_connection
          return yield(connection)
        rescue => e
          DataMapper.logger.error(e.to_s)
          raise e
        ensure
          close_connection(connection) if connection
        end
      end

さてさて,上記 yield で実行されるコードについて.

Connection から Command 作って,Command 実行して Reader で受け取り,Reader で読み取ったデータをCollection に load していくという流れ.Collection#load メソッドで Model#load メソッドが呼び出される.SQL は,DataObjectsAdapter の内部に定義された SQL#read_statement メソッドで作成される.他の各種 statement も SQL モジュールにメソッドとして定義されている.

DataObjects module

Connection とか Command とか Reader は,DataObjects モジュールに定義されている.DataObjects は github における do に定義されている.中身についてはアクセス先のストレージによりまちまちで,MySQL/PostgreSQL/SQLite の場合は ruby 拡張ライブラリとして実装されている.

終わりに

0.9.x 系は中断して,今後は 0.10.x 系のコードをメインに追っかけていきます.
なるべく図を入れるようにしよう.

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 になる).