ActiveRecordを支える技術 – Arelとは何者なのか?(全5回) その4


2014年 05月 06日

今回は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を含むsqlを生成してみる – 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ノードを返します。

07-05faf92a-0b4c-01e4-e693-3aa4f1f570e2.png

で、このInnerJoinノードを@ctx.source.rightに入れているってことは、

product.join(corporation)

実行後のノードの状態は以下のようになります。

08-3ce2943e-b8b5-745f-04a0-cde408e72702.png

joinを含むsqlを生成してみる – onの動きを把握する

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]) のノード

図にすると、以下のような感じです。

09-3a9d58cf-510c-5d74-362a-5a16a8d657ed.png

で、このノードを @ctx.source.right.last.right に入れる。
コードで見ると分かりにくいですが、図にすると以下のような感じです。

10-9a14cc09-1388-08dd-aad6-3b891d6063ee.png

joinを含むsqlを生成してみる – 複数joinした時のonの動きを把握する

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に引数で渡したノードをくっつけるからです。

図にすると分かります。

11-a8f5ef33-5e43-18d2-8880-9b2e95bf721f.png

まとめ

今回はjoinとonの動きを学びました。図にしてみると意外と単純です。

今回のメモです。

  • join関数は、source.right に InnerJoinノードを追加する
  • on関数は一番最後に実行したjoinのInnerJoinノードのrightにOnノードを追加する

次回は最終回です。