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 がモデルとクエリの変換処理を行う.