arel-3.0.2 での以下のコードの実行過程を追い掛けています。
Table#[]
次に、books[:title]
の部分について見ます。
Table::[]
は、Arel::Attribute.new
を呼びだしています。
Attribute
と Struct
について
Struct
とは、、、
構造体クラス。Struct.new はこのクラスのサブクラスを新たに生成します。 個々の構造体はサブクラスから Struct.new を使って生成します。
クラスを作るコンストラクタって何だよ…
arel/attributes/attribute.rb では以下のように使われています。
Arel::Predications
では例えば次のようなメソッドを定義しています。
- Arel::Predications#not_eq
- Arel::Predications#not_eq_any
- Arel::Predications#not_eq_all
- Arel::Predications#eq
- Arel::Predications#eq_any
つまり、Attribute
という構造体(のサブクラス)に対して、比較や算術演算などができるようになっている、ということです。
Table#[]
はこの拡張された構造体を返していることが分かります。
つまり、books[:title].relation
にはself
が、books[:title].name
には:title
が格納されます。
さらに、books[:title]
はPredications
モジュールから取り込まれたeq
というメソッドをもっています。
ちなみに、::Arel::Attribute.new
としていますが、Arel::Attribute
はArel
モジュールの定数で、
実体はArel::Attributes::Attribute
クラスです。
Predications#eq
Predications#eq
は↓
Nodes::Equality
クラスは、Binary
を継承しています。
Binary
のコンストラクタは、単に引数を記憶しておくだけです。
この時点では、具体的な処理はまだ何もしていません。
eq
メソッドはArel::Nodes::Equalitiy
クラスのインスタンスを返します。
Class.new
と const_set
話は逸れますが、binary.rbに以下のような記述がありました。
Module#const_set
は、モジュールに定数を定義するメソッドです。
Class.new(supercass = Object)
は、superclass
を親クラスとして匿名クラスをつくります。
Nodeモジュールに、Binaryクラスのサブクラスを値として、各種バイナリオペレータに対応する定数を定義しています。
ぱっとみエグいです。
Table#where
books.where
を呼びだすと、新たに SelectManager
クラスのインスタンスが生成され、
そのインスタンスが保持するwheres
というリストに、引数が追加されます。
Expressions#eq
と同様、引数を記憶しておくだけです。
SelectManager#to_sql
さて、具体的にSQLに変換している部分を見ていくことにします。
Table#where
を実行すると、SelectManager
クラスのインスタンスを返します。
SelectManager#to_sql
の呼び出しは、継承元の TreeManager
クラスに渡ります。
engine.connection.visitor
ですが、このengine
は最初に
ででてきたengine
です。
デバッガで、visitor
が現れる場所を探していくと、
に行きつきます。ActiveRecord側で何が起きているかは深く考えないことにして、
とにかく、Arel::Visitors::SQLite
を見ていくことにします。
結局、TreeManager#to_sql
は、Arel::Visitors::ToSql#accept
に行き着くことになります。
ちなみに、accept
の引数@ast
は、books.where
を呼びだした時に生成された SelectManager
クラスのインスタンスで、これがbooks.where
に渡された引数を保持しています。
Visitors::Visitor
Visitors::ToSql
は Visitors::Visitor
を継承しています。
Hash.new {|hash, key| ...}
はハッシュのデフォルト値を定めます。
ref. http://www.ruby-lang.org/ja/old-man/html/Hash.html
上のコードで、accept
に渡される引数はArel::Nodes::SelectStatement
クラスのインスタンスでした。
したがって、dispatch[object.class]
が実行された場合、得られる値は"visit_Nodes_SelectStatement"
となります。
visit
では、send
メソッドを呼んでいます。send
はObject
クラスのメソッドで、
obj.send(name,args)
は、obj
のname
メソッドをargs
を引数として呼びだします。
つまり、ToSql
クラスのvisit_Nodes_SelectStatement
というメソッドが呼ばれることになります。
ちなみに、Visitor
クラスのサブクラスには、
DepthFirst
Dot
Informix
OrderClauses
などがあります。これらのクラスのインスタンスのvisit
メソッドを呼びだしたときも、同様の処理が行われることになります。
To_Sql#visit_Nodes_SelectStatement
o.cores
の中に、books.where
に渡された引数が格納されています。
ここで察しがつくかと思いますが、ここからvisit
を再帰的に呼び出していきます。
o.wheres
に books.where
に渡された引数 books[:title].eq(...)
が格納されています。
books[:title].eq(...)
のクラスはArel::Nodes::Equality
なので、8行目でvisit_Arel_Nodes_Equality
が呼ばれます。
visit o.left
の部分で、books[:title]
に対してvisit
が呼ばれます。
最終的に、"SELECT FORM books WHERE books.title = 'Head First Rails'"
という文字列が得られるはずです。
まとめ
Arelに特徴的なのは、#where
や #eq
というメソッドが呼ばれたときに、
特に具体的な変換は行わず、引数やself
を保持するインスタンスを返すだけ、ということではないでしょうか。
いってみれば、#where
や#eq
などを使いながら「SQL構文木」を構築していくのだと思います。
#to_sql
が呼ばれたときに、この「構文木」のノードや葉を対応するSQLに変換していくわけです。