Arel::Nodes を使って書かれたコードを最低限読んで理解できるようになりたい方のために、例文を紹介しながら、どのような処理なのか読み解いていきます。
※ 自分が実際に使用されているのを見たことがあるもののみを紹介しています。
ちなみに、arel_table から作れる条件式はこちらが参考になりました。
ActiveRecordのarel_tableから作れる条件式まとめ
Arel::Nodes#build_quoted
build_quoted メソッドは文字列などの値をラップして、Arel::Nodes::NamedFunction などの引数に渡せるようにします。
Arel::Nodes.build_quoted(" ")
上記例文は " "
をラップしているだけですね。具体的な使用例は Arel::Nodes::NamedFunction を参照ください。
Arel::Nodes::NamedFunction
Arel::Nodes::NamedFunction は SQL の任意の関数を呼び出すことができます。
第1引数に関数名、第2引数に関数に渡す引数(配列)、第3引数にエイリアス名(任意)を渡します。
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`
例文では、第1引数に CONCAT
が指定されています。
CONCAT 関数は複数の文字列を結合してくれるもので、ここでは User テーブルの last_name
と " "
、 first_name
を繋ぎます。
例えば、last_name
が「Yamada」で first_name
が「Taro」だとすると「Yamada Taro」となります。
苗字と名前を分けてDBに保存しているが、つなげて取り出したい時の使い方になります。
Arel::Nodes::NamedFunction に関してはもう一つ例文を紹介します。
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 を使用しています。
Arel::Nodes::SqlLiteral.new("utf8_general_ci")
例文は utf8_general_ci
をラップしているだけですね。具体的な使用例は Arel::Nodes:InfixOperation を参照ください。
Arel::Nodes::InfixOperation
Arel::Nodes::InfixOperation の「infix operation」は二項演算という意味で、2つの値・変数の計算を行うことができます。
第1引数に演算子、第2引数に演算子の左側の値、第3引数に演算子の右側の値が入ります。
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句の第2引数に Arel::Nodes::OuterJoin
を指定し、最後に join_sources
を呼び出すことで結合できます。
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 の右側の値が入ります。
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 構文を表現することができます。使い方は直感的ですので、例文を参考にしてください。
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つの理由 という記事もあり、Arel を使うのには賛否両論があるようです。
ただ、環境によってはすでに Arel が頻繁に使用されていて、書かないまでも読む必要はあるような場面は結構多いように思います。こちらの記事がそのような環境に出くわした方の参考になれば幸いです。