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
基本的な流れは以下の通り.
- context (コンテキストスタック) を取得
- 引数に name が指定されていれば,context から名前の一致する Repository インスタンスを取得
- 引数に name が指定されていなければ,:default を name とし,context から末尾の Repository インスタンスを取得
- context から Repository インスタンスがとれなかった (nil だった) 場合は,新しい Repository インスタンスを生成
- block が渡されていれば,block 渡して Repository#scope を呼び出す
- 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 のコードを読み進めたときの反省点.
- 全体像の把握をきちんとしていなかった
- 気づいたら深さ優先になってた
- 図がない!
- 自分で読み返してもわかりづらい!
と,いうわけで,気をつけろよ!>自分
パッケージ
- dm-core
- コアパッケージ.DataMapper の基本的な機能が含まれる.DataMapper::Resource とかはここに入ってる.
- do
- Database Driver 集.DataObjects モジュールとして interface が定義されている.do_mysql や do_sqlite3 等はここに含まれている.
- dm-more
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
- よく使われるパッケージを一括して require.
名前データが大量に欲しくなって
テーブルに突っ込むデータとして,名前データが欲しくなったわけです.そしたらこんな記事を発見.
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
処理としては,
- クエリ更新
- クエリ実行 (後述するが,実際に 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
が呼び出されたときである.ここで,
- scope_stack が返す配列に query を追加
- query を引数にしてブロック呼び出し
- 終わったら 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 になる).