Arel::Nodesを最低限読めるようになりたい

Arel::Nodes を使って書かれたコードを最低限読んで理解できるようになりたい方のために、例文を紹介しながら、どのような処理なのか読み解いていきます。

※ 自分が実際に使用されているのを見たことがあるもののみを紹介しています。

ちなみに、arel_table から作れる条件式はこちらが参考になりました。
[ActiveRecordのarel_tableから作れる条件式まとめ](https://qiita.com/akicho8/items/c3bfd39213d6e9843e4f)

## Arel::Nodes#build_quoted

build_quoted メソッドは文字列などの値をラップして、Arel::Nodes::NamedFunction などの引数に渡せるようにします。

```ruby
Arel::Nodes.build_quoted(" ")
```

上記例文は `" "` をラップしているだけですね。具体的な使用例は Arel::Nodes::NamedFunction を参照ください。

## Arel::Nodes::NamedFunction

Arel::Nodes::NamedFunction は SQL の任意の関数を呼び出すことができます。
第１引数に関数名、第２引数に関数に渡す引数（配列）、第３引数にエイリアス名（任意）を渡します。

```ruby:例文１

users_table = User.arel_table
separator = Arel::Nodes.build_quoted(" ")

User.select(
  Arel::Nodes::NamedFunction.new(
    "CONCAT",
    [users_table[:last_name], separator, users_table[:first_name]],
    "name"
  )
)

# => SELECT CONCAT(`users`.`last_name`, ' ', `users`.`first_name`) AS name FROM `users`
```

例文では、第１引数に `CONCAT` が指定されています。
CONCAT 関数は複数の文字列を結合してくれるもので、ここでは User テーブルの `last_name` と `" "`、 `first_name` を繋ぎます。
例えば、`last_name` が「Yamada」で `first_name` が「Taro」だとすると「Yamada Taro」となります。
苗字と名前を分けてDBに保存しているが、つなげて取り出したい時の使い方になります。

Arel::Nodes::NamedFunction に関してはもう一つ例文を紹介します。

```ruby:例文２
users_table = User.arel_table
format = Arel::Nodes.build_quoted("%Y-%m-%d")

User.select(
  Arel::Nodes::NamedFunction.new(
    "DATE_FORMAT",
    [users_table[:created_at], format],
    "registration_date"
  )
)

# => SELECT DATE_FORMAT(`users`.`created_at`, '%Y-%m-%d') AS registration_date FROM `users`
```

DATE_FORMAT 関数は名前の通り、日時を指定のフォーマットに整形してくれる関数です。
ここでは User テーブルの `created_at` を `%Y-%m-%d` というフォーマットに変換します。
「2020-07-01」みたいな形です。
"日時"ではなく"日付"として取り出したい時の使用例になります。

## Arel::Nodes::SqlLiteral
生の SQL 文字列を作ることができます。SQL の構文中に必要な文字列をラップするのに使われることが多いです。
ちなみに、Arel で唯一パブリックな関数である `Arel.sql()` は、内部で Arel::Nodes::SqlLiteral を使用しています。

```ruby
Arel::Nodes::SqlLiteral.new("utf8_general_ci")
```

例文は `utf8_general_ci` をラップしているだけですね。具体的な使用例は Arel::Nodes:InfixOperation を参照ください。


## Arel::Nodes::InfixOperation

Arel::Nodes::InfixOperation の「infix operation」は二項演算という意味で、２つの値・変数の計算を行うことができます。
第１引数に演算子、第２引数に演算子の左側の値、第３引数に演算子の右側の値が入ります。

```ruby
User.where(
  Arel::Nodes::InfixOperation.new(
    "COLLATE",
    User.arel_table[:nickname],
    Arel::Nodes::SqlLiteral.new("utf8_general_ci")
  )
  .matches(Arel::Nodes.build_quoted("トム%"))
)

# => SELECT `users`.* FROM `users` WHERE `users`.`nickname` COLLATE utf8_general_ci LIKE 'トム%'
```

例文では演算子として `COLLATE` を指定しています。COLLATE 関数は検索条件で照合順序を指定する時に使用されるものです。そして演算子の左にくるのが User テーブルの `nickname`、右に来るのが `utf8_genera_ci` です。

ちなみに、utf8_genera_ci は UTF-8 文字コードで半角や全角、濁点を区別し、英語の大文字小文字を区別しない指定です。

つまり、User の nickname の「AAA」と「aaa」は一緒とみなして、「あああ」と「アアア」や「ぁぁぁ」は区別するということになります。

そして、最後の `matches()` で `トム%` を指定しているため、この例文は nickname が「トム」から始まる User を取得するというクエリになります。このとき、"とむ" や "ドム" などから始まる User はヒットしません。



## Arel::Nodes::OuterJoin
外部結合（OUTER JOIN）する際に使用します。使い方としては、JOIN句の第２引数に `Arel::Nodes::OuterJoin` を指定し、最後に `join_sources` を呼び出すことで結合できます。

```ruby
messages_table = Message.arel_table
users_table = User.arel_table

Message.joins(
  messages_table
    .join(users_table, Arel::Nodes::OuterJoin)
    .on(
      messages_table[:user_id].eq(users_table[:id])
      .and(users_table[:is_active].eq(true))
    )
    .join_sources
)

# => SELECT `messages`.* FROM `messages` LEFT OUTER JOIN `users` ON `messages`.`user_id` = `users`.`id` AND `users`.`is_active` = TRUE
```

例文では、Message テーブルに User テーブルを外部結合しています。
結合の条件としては、Message テーブルの `user_id` と User テーブルの `id` が等しい、かつ User テーブルの `is_active` が `true` のものという条件になります。
つまり、active な状態の User のみ外部結合するという条件になっているわけです。

## Arel::Nodes::Descending, Arel::Nodes::Ascending
Arel::Nodes::Descending は ORDER 句の DESC （降順）の部分を担当します。Arel::Nodes::Ascending は ASC （昇順）です。引数には、ORDER BY の右側の値が入ります。

```ruby
User.order(
  Arel::Nodes::Descending.new(
    User.arel_table[:user_type].in([10,11,12])
  )
)

# => SELECT `users`.* FROM `users` ORDER BY `users`.`user_type` IN (10, 11, 12) DESC
```

例文では、Arel::Nodes::Descending の引数に `User.arel_table[:user_type].in([10,11,12])` が指定されています。
in 関数がついていなければ、この例文は `User.order(user_type: :desc)` と同じクエリになり `user_type` の降順に並ぶわけですが、`.in([10,11,12])` と絞り込むことで、これらを一つのグループとして並び替えています。
この場合は `user_type` が 10, 11, 12 のユーザー群が前方に、それ以外のユーザー群が後方に来るように並びます。
前方と後方に分かれた後の並びは特に指定していないので、id の昇順になります。
特定の条件に合致するユーザーを前方に持ってきたいときなどに使用されますね。

## Arel::Nodes::Case
SQL の CASE 構文を表現することができます。使い方は直感的ですので、例文を参考にしてください。

```ruby
products_table = Product.arel_table
new_cond = products_table[:created_at].gt(Time.zone.now - 3.days)

Product.order(
  Arel::Nodes::Ascending.new(
    Arel::Nodes::Case.new
      .when(new_cond).then(1)
      .else(9)
  )
)

# => SELECT `products`.* FROM `products` ORDER BY CASE WHEN `products`.`created_at` > '2020-07-01 12:34:56' THEN 1 ELSE 9 END ASC
```

`Arel::Nodes::Case.new` の箇所では、Product が作成されて3日以内であれば `1`、そうでなければ `9` という値をとります。これと Arel::Nodes::Ascending を組み合わせることで、3日以内に作成された Product を前方にそれ以外を後方に並び替えています。

「Arel::Nodes::Descending, Arel::Nodes::Ascending」 の例文と同じ感じですね。


## 最後に
例文で見たように Arel::Nodes を使うことで表現力はかなりアップします。しかし、慣れていない方にとっては何を行っているのかわかりづらいという欠点もあります。また、[Arelでクエリを書くのはやめた方が良い5つの理由](https://qiita.com/jnchito/items/630b9f038c87298b5756) という記事もあり、Arel を使うのには賛否両論があるようです。

ただ、環境によってはすでに Arel が頻繁に使用されていて、書かないまでも読む必要はあるような場面は結構多いように思います。こちらの記事がそのような環境に出くわした方の参考になれば幸いです。

### 参考
- [Arel::Nodes](https://www.rubydoc.info/gems/arel/Arel/Nodes)
- [Arel::Nodes を使って Arel で複雑な SQL文を作っちゃおう](https://qiita.com/Ping/items/87148251fe9bec2a847e#arelnodesnamedfunction)
- [【MySQL】照合順序とは？](https://qiita.com/kazu56/items/6af85ffcf8d3954455ad)