DataMapper Reading (その 1: module DataMapper)

DataMapper モジュールには,setup や repository メソッド,migration 関連メソッドが定義されている.

対象バージョン

0.9.11

DataMapper.setup メソッド

# 使用例
DataMapper.setup(:default, 'mysql://localhost/hoge_db')

DataMapper を使うときに必ず呼び出すメソッド.第 2 引数に対応する Adapter インスタンスを生成し,第 1 引数と関連づける.戻り値は生成した Adapter インスタンス

module DataMapper
  self.setup(name, uri_or_options)
    def self.setup(name, uri_or_options)
      ...
      case uri_or_options
        when Hash
          adapter_name = uri_or_options[:adapter].to_s
        when String, DataObjects::URI, Addressable::URI
          uri_or_options = DataObjects::URI.parse(uri_or_options) if uri_or_options.kind_of?(String)
          adapter_name = uri_or_options.scheme
      end

      class_name = Extlib::Inflection.classify(adapter_name) + 'Adapter'

      unless Adapters::const_defined?(class_name)
        lib_name = "#{Extlib::Inflection.underscore(adapter_name)}_adapter"
        ...(略)
        # 以下の 2 つを試みて,両方とも失敗したら例外を raise
        # require root / 'lib' / 'dm-core' / 'adapters' / lib_name
        # require lib_name
      end

      Repository.adapters[name] = Adapters::const_get(class_name).new(name, uri_or_options)
    end

まずは,引数の uri_or_options から adapter_name を取得する.
引数が Hash であれば :adapter をとるが,String なら DataObjects::URI.parse して URI に変換したうえで schema をとる.その他 (URI) ならそのまま schema をとる.通常は,使用例のように String,もしくは Hash であることが多いはず.
(DataObjects::URI は data_objects-0.9.12/lib/data_objects/uri.rb に定義されている.実質中身は Addressable::URI に等しい.Addressable::URI は addressable-2.0.2/lib/addressable/uri.rb に定義されている.)


次に adapter_name から Adapter クラスを特定しなければならない.これは,

      class_name = Extlib::Inflection.classify(adapter_name) + 'Adapter'

の部分で行う.
Extlib::Inflaction.classify は,引数を単数形にしたうえで,'_' 区切りを先頭大文字区切りに変換する (例: aaa_bbb → AaaBbb).
(extlib-0.9.12/lib/extlib/inflections.rb に定義されているため,詳細はそちらを)


続いて,得られた class_name クラスが定義済みでなければ,所定の場所から読み込む.
1 回目に読み込む場所は決まっていて,${gems のルート}/dm-core-${バージョン}/lib/dm-core/adapters 以下から読み込もうとする.なかったら普通に lib_name で require する.


最後に,Module.const_get でクラス定数を取得し,new して Repository.adapters[name] につっこんでおく.


setup により生成された Adapter インスタンスは,全て Repository.adapters に保持される.接続先 DB については Adapter 自身が保持している (Adapter.new の引数に uri_or_options が渡されている).

Adapter は,実際にモデルをテーブルと対応付ける役割を担っている.Adapter のインタフェースは AbstractAdapter により規定されており (create/update/read_many/read_one/delete 等),インタフェース毎にモデルから適切なクエリを生成して実行する.

DataMapper.repository メソッド

# 使用例
DataMapper.setup(:hoge, 'mysql://localhost/hoge_db')
p = DataMapper.repository(:hoge) { Person.first }

引数で指定された name に対応する Repository インスタンスを返す.ブロックが渡された場合は,ブロックの実行結果を返す.ここの name には,setup の name と同じものを指定する.

Repository インスタンスは,主に以下の情報を管理している.

  • 接続先 DB 用 Adapter
  • Identity Map
    • ↑テーブルから取得されたモデルデータが格納される Map
module DataMapper
  def self.repository(name = nil) # :yields: current_context
    current_repository = if name
      raise ArgumentError, "First optional argument must be a Symbol, but was #{name.inspect}" unless name.is_a?(Symbol)
      Repository.context.detect { |r| r.name == name } || Repository.new(name)
    else
      Repository.context.last || Repository.new(Repository.default_name)
    end

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

まず最初に,name をキーにして Repository.context から生成済 Repository インスタンスの取得を試みる.無ければ新しく生成する.Repository.context はスレッドローカルの配列を返す.Repository.default_name は :default を返す.

Repository.context が空でないのは,repository(:hoge) { repository } の様に,ブロック内部で呼び出されたときだけ (ブロック内部でモデルのクラスメソッドを呼び出すと,repository が間接的に呼び出される).これは,ブロックありで呼び出すと内部で Repository#scope メソッドが呼び出されるため (後述).
大抵の場合,DataMapper.repository メソッドの呼び出しはブロック無しであるため,Repository.context は空配列であり,新しい Repository インスタンスが作成されることになる (DataMapper.repository が呼び出されるのは,モデルのクラスメソッド内部であり,取得した Repository インスタンスは,モデルのクラスメソッドの戻り値であるモデルインスタンスインスタンス変数に保持される).Identity Map は Repository インスタンスが保持しているため,つまりは Identity Map も新しく作成されることになる.


最後は,repository メソッドにブロックが渡されたかどうかによって処理が分かれる.
ブロックがある場合は,Repository#scope にブロックの実行を依頼する.Repository#scope メソッドは以下の通り.

module DataMapper
  class Repository
    def scope
      Repository.context << self

      begin
        return yield(self)
      ensure
        Repository.context.pop
      end
    end

コンテキストを積んだ状態でブロック呼び出しを行う.

前述の使用例のように

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

とした場合,Persion.first は :hoge に関連づけられた Repository 上で実行される.
(レコードからモデルへのマッピング処理は,Model#repository が返す Repository インスタンス上で行われる.Model#repository は内部で DataMapper.repository を呼び出す.)


ブロックがない場合は,取得済みの Repository インスタンスを返す.

migration 関連メソッド

module DataMapper
  def self.migrate!(name = Repository.default_name)
    repository(name).migrate!
  end

Repository#migrate! は,Repository::Migration モジュールを include して定義されている (Migration は移動予定らしい.0.10.0 だと移動してるのかな?).
このメソッドの内部では Migrator.migrate が呼ばれる.
TODO Migration で説明

module DataMapper
  def self.auto_migrate!(repository_name = nil)
    AutoMigrator.auto_migrate(repository_name)
  end

TODO Migration で説明

module DataMapper
  def self.auto_upgrade!(repository_name = nil)
    AutoMigrator.auto_upgrade(repository_name)
  end

TODO Migration で説明

その他

module DataMapper
  def self.prepare(*args, &blk)
    yield repository(*args)
  end

  def self.dependency_queue
    @dependency_queue ||= DependencyQueue.new
  end

TODO 説明