毛無しさん@キレートレモン

Arel #2

arel-3.0.2 での以下のコードの実行過程を追い掛けています。

Table#[]

次に、books[:title] の部分について見ます。

Table::[]は、Arel::Attribute.newを呼びだしています。

AttributeStruct について

Structとは、、、

構造体クラス。Struct.new はこのクラスのサブクラスを新たに生成します。 個々の構造体はサブクラスから Struct.new を使って生成します。

ref. http://doc.ruby-lang.org/ja/1.9.3/class/Struct.html

クラスを作るコンストラクタって何だよ…

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::AttributeArelモジュールの定数で、 実体はArel::Attributes::Attributeクラスです。

Predications#eq

Predications#eqは↓

Nodes::Equalityクラスは、Binaryを継承しています。

Binaryのコンストラクタは、単に引数を記憶しておくだけです。

この時点では、具体的な処理はまだ何もしていません。

eqメソッドはArel::Nodes::Equalitiyクラスのインスタンスを返します。

Class.newconst_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::ToSqlVisitors::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メソッドを呼んでいます。sendObjectクラスのメソッドで、 obj.send(name,args)は、objnameメソッドをargsを引数として呼びだします。

つまり、ToSqlクラスのvisit_Nodes_SelectStatementというメソッドが呼ばれることになります。

ちなみに、Visitorクラスのサブクラスには、

  • DepthFirst
  • Dot
  • Informix
  • OrderClauses

などがあります。これらのクラスのインスタンスのvisitメソッドを呼びだしたときも、同様の処理が行われることになります。

To_Sql#visit_Nodes_SelectStatement

o.coresの中に、books.whereに渡された引数が格納されています。

ここで察しがつくかと思いますが、ここからvisitを再帰的に呼び出していきます。

o.wheresbooks.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に変換していくわけです。