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


2014年 05月 05日

今回は以下のようなSQL生成の過程を学んでみます。

product = Arel::Table.new(:products)
product.project('*').where(product[:price].eq(1000)).to_sql
# "SELECT * FROM `products` WHERE `products`.`price` = 1000"

まずは

product[:price].eq(1000)

の部分から読んでみます。

以下、目次となります。

whereを含むSQLを生成してみる – where関数の引数は何?

product[:price]ってなんでしょう。
[] 関数は、lib/arel/table.rbに定義されています。

module Arel
  class Table
    # 色々略しています

    def [] name
      ::Arel::Attribute.new self, name
    end
  end
end

どうやら単にArel::Attributeのインスタンスを返しているようです。
pryで見てみましょう。

[8] pry(main)> product[:price]
=> #<struct Arel::Attributes::Attribute
 relation=
  #<Arel::Table:0x007f994e018d78
   @aliases=[],
   @columns=nil,
   @engine=ActiveRecord::Base,
   @name="products",
   @primary_key=nil,
   @table_alias=nil>,
 name=:price>

Attributeクラスとはなんでしょう。ソースを見てみます。
lib/arel/attributtes/attribute.rbです。

module Arel
  module Attributes
    class Attribute < Struct.new :relation, :name
      include Arel::Expressions
      include Arel::Predications
      include Arel::AliasPredication
      include Arel::OrderPredications
      include Arel::Math

      ###
      # Create a node for lowering this attribute
      def lower
        relation.lower self
      end
    end

    class String    < Attribute; end
    class Time      < Attribute; end
    class Boolean   < Attribute; end
    class Decimal   < Attribute; end
    class Float     < Attribute; end
    class Integer   < Attribute; end
    class Undefined < Attribute; end
  end

  Attribute = Attributes::Attribute
end

relationとnameという変数を持つクラスのようです。
なにやら色々includesしていますね。どんな関数を持っているのか、pryで見てみます。

04-71103046-fac8-fb63-3562-6e57d5852856.png

eq 関数は Arel::Predications に定義されているようです。ということで、今度は lib/arel/predications.rb を見てみます。

module Arel
  module Predications
    # 色々略
    def eq other
      Nodes::Equality.new self, Nodes.build_quoted(other, self)
    end
 end
end

Nodes::Equalityのインスタンスを作っているようです。
こいつは単にleft, rightという変数を持っているだけのクラスです。
initializeの第一引数はleftに、第二引数はrightに値をいれています。

ということは、EqualityのleftにはAttributes::Attributeが、rightには other(今回の場合は1000) が入る事となります。

よって、

product[:price].eq(1000)

という操作を行うことで、以下のような構造を持ったArel::Nodes::Equalityが取得できます。

05-0bd98b7a-4135-b003-05eb-9ab9a32f058d.png

なお、Arel::Nodes は to_sqlが実行可能です。

[2] pry(main)> product[:price].eq(1000).to_sql
=> "`products`.`price` = 1000"

WHERE句の中身が取得できましたね。

whereを含むSQLを生成してみる – whereを読む

whereは前回のproject関数とほぼ同じような実装です。
単にSelectCoreノードのwheresに先ほどのEqualityノードを入れてるだけです。
where関数の実装は、lib/arel/tree_manager.rb にあります。

# いろいろ省略
module Arel
  class TreeManager
    include Arel::FactoryMethods

    def where expr
      if Arel::TreeManager === expr
        expr = expr.ast
      end
      @ctx.wheres << expr
      self
    end
  end
end

@ctxは前回でてきましたよね。@ast.cores.lastです。

最初に戻って。以下のコード実行後のSelectManagerの@astの状態は以下の図のようになります。

product.project('*').where(product[:price].eq(1000))
# "SELECT * FROM `products` WHERE `products`.`price` = 1000"
06-14ba6358-4dd0-9593-73f3-fe571b8c914a.png

まとめ

今回は単純でした。whereの実装は前回のprojectの仕組みとほぼ同じ感じです。

今回のメモです。

  • where関数の引数にはArel::Nodes型のオブジェクトが渡せる
  • Arel::Node型はそれ単体でto_sqlして評価が可能
  • SelectManagerのwhereを呼び出すと、内部のSelectStatement内のSelectCore, @wheres変数に引数で渡された値を入れる
  • where関数を呼び出しても戻り値はSelectManagerのままなのでメソッドチェーン可能。whereやprojectがチェーンできる。