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クラスのサブクラスには、
DepthFirstDotInformixOrderClauses
などがあります。これらのクラスのインスタンスの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に変換していくわけです。