DataMapper 0.10.2 Reading (その 4: dm-core/core_ext/)

このディレクトリに配置されたコードは,ruby の標準クラス (モジュール) を拡張するためのもの.具体的には以下の 3 つが拡張されている.

  • Enumerable
  • Kernel
  • Symbol

それぞれは小さなコードだが,影響範囲は広い.

Enumerable

empty?/one?/first/size メソッドを定義している.標準メソッドについては こちら を参照されたし.
Enumerable の規約に従い,全て each により定義されている.これらの内,ここでは one? メソッドを取り上げようと思う.one? メソッドは文字通り「1 つだけ?」かどうかを判定するわけだが,この判定基準にブロックを用いる.

module Enumerable
  def one?
    return one? { |entry| entry } unless block_given?

    matches = 0
    each do |entry|
      matches += 1 if yield(entry)
      return false if matches > 1
    end
    matches == 1
  end

与えられたブロックで各要素を評価し,true を返した要素の個数をカウントしていく.カウントが 1 であれば true を返すし,そうでなければ false を返す.

Kernel

Kernel では,DataMapper モジュールの repository メソッドを呼び出すためのメソッドが定義されている.

module Kernel
  private
  def repository(*args, &block)
    DataMapper.repository(*args, &block)
  end
end

ここで定義されているということは,self.repository の形であれば (self 以外に対して .repository としなければ) どこでも呼び出せる.
簡単な例を示すと,以下のような感じ.

module Kernel
  private
  def hoge
    puts "hogeっていうな"
  end
end

hoge
# => hogeっていうな

class A; end
a = A.new
a.hoge
# => NoMethodError

class A
  def call_hoge
    hoge
  end
end
a.call_hoge
# => hogeっていうな

Symbol

コードは短いが,検索に広く影響する条件用の Symbol を定義している.

class Symbol
  (DataMapper::Query::Conditions::Comparison.slugs | [ :not, :asc, :desc ]).each do |sym|
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def #{sym}
        #{"warn \"explicit use of '#{sym}' operator is deprecated (#{caller[0]})\"" if sym == :eql || sym == :in}
        DataMapper::Query::Operator.new(self, #{sym.inspect})
      end
    RUBY
  end
end

例えば,

class MyModel
  include DataMapper::Resource
  property :id, Serial
  property :name, String
end

というモデルを定義したとき,

m = MyModel.all(:name.like => 'Krd%')

という呼び出しの ".like" にあたる部分を定義していることになる.
この定義により,".like" と呼び出すことで,条件演算を表す Operator インスタンスが得られる.

なお,"Comparison.slugs" については後ほど.ここでは「条件記述用に定義された Symbol を配列で返す」ぐらいの認識でよいかと.

Pocket WiFi

Pocket WiFi なるものを購入してみた.

思ってたよりも小さい.スゲーつるつるしてる.

プランは「新にねん」で「スーパーライトデータ」にした.これだと本体価格は 15,580 円になる.「にねん M」だと 5,980 円になるが,差額の 9,600 円は 24ヶ月で分割され,月々の基本料金に上乗せされる.まぁ,つまるところ,2 年間使い続ければ差は無くなる.2 年より多く使うのであれば,多少は「新にねん」の方がお得になる.


これからいろいろ持ち運んでみる予定.

追記:2010/02/02
http://speedtest.goo.ne.jp/ で計測してみたら 0.51 Mbps だった...あれ?

DataMapper 0.10.2 Reading (その 3: dm-core/)

今回からは dm-core/ 以下のコードを見ていきます.

配置確認

うーん,かなり変更されている...

dm-core/
  adapters/
    abstract_adapter.rb
    data_objects_adapter.rb
    in_memory_adapter.rb
    mysql_adapter.rb
    oracle_adapter.rb
    postgres_adapter.rb
    sqlite3_adapter.rb
    sqlserver_adapter.rb
    yaml_adapter.rb
  associations/
    many_to_many.rb
    many_to_one.rb
    one_to_many.rb
    one_to_one.rb
    relationship.rb
  core_ext/
    enumerable.rb
    kernel.rb
    symbol.rb
  model/
    descendant_set.rb
    hook.rb
    is.rb
    property.rb
    relationship.rb
    scope.rb
  query/
    conditions/
      comparison.rb
      operation.rb
    direction.rb
    operator.rb
    path.rb
    sort.rb
  spec/
    adapter_shared_spec.rb
    data_objects_adapter_shared_spec.rb
  support/
    chainable.rb
    deprecate.rb
    equalizer.rb
    logger.rb
    naming_conventions.rb
  types/
    boolean.rb
    discriminator.rb
    object.rb
    paranoid_boolean.rb
    paranoid_datetime.rb
    serial.rb
    text.rb
  adapters.rb
  collection.rb
  identity_map.rb
  migrations.rb
  model.rb
  property.rb
  property_set.rb
  query.rb
  repository.rb
  resource.rb
  transaction.rb
  type.rb
  version.rb

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 を返すための処置である.

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

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