今回はArelがjoinクエリを生成する過程を学んでみます。
使用するサンプルコードは以下の通りです。
product = Arel::Table.new(:products)
corporation = Arel::Table.new(:corporations)
product_detail = Arel::Table.new(:product_details)
product
.project('*')
.join(corporation)
.on(product[:corporation_id].eq(corporation[:id]))
.to_sql
# "SELECT * FROM `products`
# INNER JOIN `corporations` ON `products`.`corporation_id` = `corporations`.`id`"
product
.project('*')
.join(corporation)
.on(product[:corporation_id].eq(corporation[:id]))
.join(product_detail)
.on(product[:id].eq(product_detail[:id]))
.to_sql
# "SELECT * FROM `products`
# INNER JOIN `corporations` ON `products`.`corporation_id` = `corporations`.`id`
# INNER JOIN `product_details` ON `products`.`id` = `product_details`.`id`"
まずは、joinがどういう動きをするのか、から見ていきましょう。
以下、目次となります。
join関数の第一引数にはArel::TableやNode, String等なんでも渡せます。
product.join(corporation).to_sql
# "SELECT FROM `products` INNER JOIN `corporations`"
product.join(corporation.project('*')).to_sql
# "SELECT FROM `products` INNER JOIN (SELECT * FROM `corporations`)"
product.join('INNER JOIN testtest').to_sql
# "SELECT FROM `products` 'INNER JOIN testtest'"
join関数は lib/arel/table.rb に定義されています。
# lib/arel/table.rb から抜粋
def join relation, klass = Nodes::InnerJoin
return from(self) unless relation
case relation
when String, Nodes::SqlLiteral
raise if relation.blank?
klass = Nodes::StringJoin
end
from(self).join(relation, klass)
end
from(self) で返ってくるのは、SelectManagerでしたよね。
なので、次はSelectManagerのjoinを見てみます。
SelectManagerのjoinは、lib/arel/select_manager.rb に定義されてます。
# lib/arel/select_manager.rb より抜粋
def join relation, klass = Nodes::InnerJoin
return self unless relation
case relation
when String, Nodes::SqlLiteral
raise if relation.blank?
klass = Nodes::StringJoin
end
@ctx.source.right << create_join(relation, nil, klass)
self
end
あれ、似たようなコードが。。。
実装は凄く単純ですよね。
@ctx.source.right に create_joinした結果を入れているだけです。
create_joinは、下図のようなArel::Nodes::InnerJoinノードを返します。
で、このInnerJoinノードを@ctx.source.rightに入れているってことは、
product.join(corporation)
実行後のノードの状態は以下のようになります。
joinの動きが分かったところで、次はonの動きをみてみます。
onにもNodeやStringが渡せます。
product.join(corporation).on('test = test').to_sql
# "SELECT FROM `products` INNER JOIN `corporations` ON test = test"
product
.join(corporation)
.on(product[:corporation_id].eq(corporation[:id]))
.to_sql
# "SELECT FROM `products`
# INNER JOIN `corporations` ON `products`.`corporation_id` = `corporations`.`id`"
product.project('*').on('test = test').to_sql
# joinしてないのにonは実行できない。以下のエラーが発生
# NoMethodError: undefined method `right=' for nil:NilClass
以下のコード、
.on(product[:corporation_id].eq(corporation[:id]))
を例に、onの動きをみていきます。
on関数も lib/arel/select_manager.rb に定義されています。
def on *exprs
@ctx.source.right.last.right = Nodes::On.new(collapse(exprs))
self
end
collapse(exprs) は複数渡したexprsをひとつのノードにまとめてるだけです。
今回は引数ひとつのノード(Arel::Nodes::Equality)を渡しているだけなので、ここは気にしないこととします。
Nodes::Onを生成しています。
Onノードはexprというひとつの変数を持っただけのノードです。
Onノードだけで評価すると、以下のようなsql文が取得できます。
Nodes::On.new(collapse(exprs)).to_sql
# "ON `products`.`corporation_id` = `corporations`.`id`"
# collapse(exprs) は product[:corporation_id].eq(corporation[:id]) のノード
図にすると、以下のような感じです。
で、このノードを @ctx.source.right.last.right に入れる。
コードで見ると分かりにくいですが、図にすると以下のような感じです。
onノードは、source.right.last.right に onの引数で渡したノードをくっつけていましたよね。
なぜlast.right?
joinは複数回実行可能です。例えば以下のようなコードが書けます。
product
.project('*')
.join(corporation)
.on(product[:corporation_id].eq(corporation[:id])) #(1回目のon)
.join(product_detail)
.on(product[:id].eq(product_detail[:id])) #(2回目のon)
.to_sql
# "SELECT * FROM `products`
# INNER JOIN `corporations` ON `products`.`corporation_id` = `corporations`.`id`
# INNER JOIN `product_details` ON `products`.`id` = `product_details`.`id`"
last.rightなのは、onは一番最後に実行したjoinのrightに引数で渡したノードをくっつけるからです。
図にすると分かります。
今回はjoinとonの動きを学びました。図にしてみると意外と単純です。
今回のメモです。
次回は最終回です。