RailsのArelを調査してみた

RailsのActiveRecordで想定通りのSQLクエリが発行されずに辛い思いをしたので、SQLまわりのソースコードを読んでいたら`Arel`というものに当たりました。

どうやらこれがrailsっぽくSQLクエリを書ける機能を提供しているようなので、勉強したことをまとめます。あまり細かいことは気にせずに、Arelを読み解く上での基礎を理解することが目標です。

ソースコードをちゃんと読んだ経験が少ないのでとんちんかんなこと書いてないか不安ですが、温かい目で見てもらえればと思います。

間違っている箇所あればコメント欄よりお願いします。

# Arelとは
ArelはActiveRecordの内側で`where`や`select`のメソッドチェーンからSQLを生成する役割を担っています。またDBによってSQLの文法が若干違ったりしますが、その差異を吸収してくれるのもArelです。

[rubygems](https://rubygems.org/gems/arel)によるとArelは、

0. Arel Really Exasperates Logicians（Logician＝論理学者)
1. 複雑なSQLクエリの生成を簡単にできるようにする
2. 色んなRDBMSに対応できるようにする
3. フレームワーク(特にORM)のフレームワークとして開発された

だそうです。

Arelはrailsやactiverecordとはまだ別のgemとして[rubygems](https://rubygems.org/gems/arel)からダウンロードするのが基本的な使い方です。
ですがゆくゆくはactiverecordに取り込まれることになっており、既に[railsのmasterブランチ](https://github.com/rails/rails/tree/master/activerecord/lib/arel)にはarelが取り込まれて（[Merge時のPR](https://github.com/rails/rails/pull/32097)）開発もそちらで進んでいるようです(元は[別レポジトリ](https://github.com/rails/arel)。)

そもそもが内部APIであるためか[公式ドキュメント](https://www.rubydoc.info/github/rails/arel)はあんまり充実していないので、使いこなすには自分でソースコードを読んでいくしかなさそうです。
また人によるかも知れませんが、Arelの勉強をする場合はActiveRecordを含まないArel単体でまず見ていく方が理解しやすいです。使い方としては、

```ruby
users = Arel::Table.new(:users)
query = users.project("id").where(users[:age].gt(20).and(users[:age].lt(30))).order("id")
query.to_sql
# "SELECT id FROM \"users\" WHERE \"users\".\"age\" > 20 AND \"users\".\"age\" < 30 ORDER BY id"
```

みたいにして簡単にクエリを生成できます。`to_sql`で対応するSQLが簡単に得られていることが分かります。
注：記事のarelのバージョンは[rubygems](https://rubygems.org/gems/arel)においてある最新版の9.0.0です。

## 主役はArel::SelectManager
`Arel::Table`の実質機能を全て管理しています。
`Arel::Table#project`や`Arel::Table#where`を呼ぶと、内部的にはすぐに`Arel::Table#from`を呼んで処理を移譲します。ちなみに`project`は日本語の「プロジェクト」の意味ではなく、「射影」と言う意味です。「SQLの`select`をarelでは`project`と呼ぶ」と理解して良いと思います。

```ruby:arel/table.rb
def from
  SelectManager.new(self)
end

def where condition
  from.where condition
end

def project *things
  from.project(*things)
end
```

この`Arel::Table#from`は`Arel::SelectManager`を返し、以後`project`や`where`のチェーンは同じ`Arel::SelectManager`オブジェクトが返されて実現します。
(つまり`users.project("id")`は`users.from.project("id")`と全く同じということです。)
arelの該当ソースコードは下記(最終行に注目してください)

```ruby:arel/tree_manager.rb
# Arel::SelectManagerの親クラス
def where expr
  if Arel::TreeManager === expr
    expr = expr.ast
  end
  @ctx.wheres << expr
  self #　自分を返している = チェーン可能
end
```

```ruby:arel/select_manager.rb
def project *projections
  # FIXME: converting these to SQLLiterals is probably not good, but
  # rails tests require it.
  @ctx.projections.concat projections.map { |x|
    STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
  }
  self #　自分を返している = チェーン可能
end
```

## Arel::SelectManagerの中にはコア(核)がある
`Arel::SelectManager`は、`@ctx`という変数で1つのcore(`Arel::Nodes::SelectCore`)を持っています。
そして1つの`core`は、`projections`, `wheres`, `groups`, `havings`などの配列と、`source`という[二分木](https://ja.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%9C%A8)を持ちます。本当は複数のコアがあることもあるのですが、この記事の中ではコアは1つしかないものとして考えることにしましょう。
これを組み立てて1つのSQL文が作られます。作られるSQL文はいつでも`Arel::TreeManager#to_sql`で確認出来ます。（`Arel::TreeManager#to_sql`は`Arel;::SelectManager`の親クラスです。）
`project`をチェーンに繋げることは`projections`配列に選択列を追加していくことになり、`where`をチェーンに繋げることは`wheres`配列に条件を追加していくこととなります。

ここで`source`は二分木と言いましたが、`Arel`では他でも二分木を頻繁に使います。
`Arel`での二分木の伸び方は、自身のノード(メソッド呼び出し元)を左に、(メソッド)引数の木を右側に持つ新しいノードを作る、という形式でrootの方向に伸びます。
イメージ的には、

```ruby
new_node = left_node.create_node(right_node)
new_node.left == left_node # true
new_node.right == right_node # true
```
みたいな感じです。
ノードのクラスはいくつかありますが、ほぼ`Arel::Nodes::Node`かそれを継承したクラスのインスタンスです。
継承したノードクラスというのは主に、`Binary`と`Unary`です。`Unary`クラスは名前の通り1つしか子を持ちません。

さて、コードを見て行きましょう。

## このコードの理解が目標です

```ruby
users = Arel::Table.new(:usrs)
users.where(
  users[:age].eq(20).or(users[:age].eq(18)).and(users[:age].gt(15))
)
```
のwhereの中身について考えてみましょう。よく読むと分かるかもしれませんが、最終的なSQLとしては

```sql
users.age = 20 OR users.age =18 AND users.age > 15
```
に対応します。またそれは、

```ruby
users.from.to_sql # "SELECT FROM \"users\""
users[:age].eq(20).or(users[:age].eq(18)).and(users[:age].gt(15)).to_sql
# "(\"users\".\"age\" = 20 OR \"users\".\"age\" = 18) AND \"users\".\"age\" > 15"
```

でも分かります。（`from`を付けているのは、`to_sql`メソッドが用意されているのが`Arel::SelectManager`だからです。）


## Arelを理解するにはまずノードから
前述しましたが、Arelでは二分木がたくさんでてきます。
上の例で出てきた`eq`や`or`などは二分木を成長させるメソッドで、引数を右側に、呼び出し元を左とする二分木のrootを生成して返します。
二分木のノードのうち子を持つノードは全て`Arel::Nodes::Node`かそれを継承するクラスのインスタンスです。
`eq`は`Arel::Nodes::Equality`(Binaryノード)を、`or`は`Arel::Nodes::Grouping`(Unaryノード)を生成して返します。

まず、

```ruby:小さな木
users[:age].eq(20)
```
を見てみましょう。

結論から言うとこの式は全体として、左側に`users[:age]`(`Arel::Attributes::Attribute`)、右側に`20`(`Arel::Nodes::Casted`)をもつ木(`Arel::Nodes::Equality`)を作ります。`Arel::Attributes::Attribute`は子を持ちません。

`Arel::Attributes::Attribute`は`Struct(:relation, :name)`を継承しており、`users[:age]`の場合は`relation`として`users`を、`name`として`"age"`をもちます。 
`Arel::Nodes::Casted`は一応`Arel::Nodes::Node`を継承しているのでノードとして振舞えるし子も持てるのですが、基本的にただの値（この場合は`20`）と思って良いと思います。

`Arel::Attributes::Attribute`や`Arel::Nodes::Casted`のように、`Arel`では一見単純なrubyのオブジェクトや配列で良さそうなものでも自作クラスのインスタンスにすることが多い印象です。

さて`users[:age].eq(20)`の返り値は`Arel::Attributes::Attribute#eq`（正確にはこのクラスにincludeされている`Arel::Predications#eq`）の返り値ですが、前述の通りこれは`Arel::Nodes::Equality`という小さな木を返します。

```ruby
eql_node = users[:age].eq(20)
eql_node.class # Arel::Nodes::Equality
eql_node.left
# <struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007fd378afb1c8 @name="users", @type_caster=nil, @table_alias=nil>, name=:age>

eql_node.right
# <Arel::Nodes::Casted:0x007fd37a401118 @val=20, @attribute=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x007fd378afb1c8 @name="users", @type_caster=nil, @table_alias=nil>, name=:age>>
```

先に進みます。

```ruby:ちょっと成長した木
users[:age].eq(20).or(users[:age].eq(18))
```

次に`Arel::Nodes::Node#or`が呼ばれて、引数の`users[:age].eq(18)`を右側に、`users[:age].eq(20)`を左側にもつ`Arel::Nodes::Grouping`（`Unary`クラスから継承）ノードを返します。

```ruby:arel/nodes/node.rb
def or right
  Nodes::Grouping.new Nodes::Or.new(self, right)
end
```

```ruby:arel/nodes/binary.rb
# Arel::Noees::Orの親クラス
def initialize left, right
  super()
  @left  = left
  @right = right
end
```

```ruby:もっと成長した木
users[:age].eq(20).or(users[:age].eq(18)).and(users[:age].gt(15))
```

最後に`Arel::Nodes::Node#and`が呼ばれて、`users[:age].gt(15)`(`Arel::Nodes::GreaterThan`(Binaryから継承))を右側に、呼び出し元の木を左側にもつ`Arel::Nodes::And`(`Arel::Nodes::Node`クラスから継承)を返します。`Arel::Nodes::And`も持つ子は2つで、`left`と`right`メソッドで呼び出せるし`Binary`と仕組みも一緒です。

次回はデザインパターンとしての`Visitorパターン`を利用している`#to_sql`の仕組みについて説明したいと思います。（現在執筆中）

参考文献
1. [技術評論社 第43回　Rails 3を支える名脇役たち その1 - Arel] (http://gihyo.jp/dev/serial/01/ruby/0043)
2. [タイムインターメディア ActiveRecordを支える技術 - Arelとは何者なのか? (全5回)](http://labs.timedia.co.jp/2014/05/activerecord---arel-5-1.html)
3. [Qiita Arelでクエリを書くのはやめた方が良い5つの理由](https://qiita.com/jnchito/items/630b9f038c87298b5756)