こんにちは。フォースタートアップス株式会社、エンジニアの石田です。
前々回、前回とRubyの話をしました。今回もRubyの話です。Active RecordのArel
を使い尽くす、という内容です。お付き合いください。
注意
この記事は「Arel
を使わないといけなくなったときに参考になるように」という目的で書きました。「Arel
を積極的に使っていこう」という趣旨ではないのでご留意ください。詳しくは次章にて。
そもそもArel
を使うことについて
Arel
はRailsの内部APIです。内部APIであるがゆえにRailsユーザーがこれを使うことは公式で推奨されていません*1 *2。SQLはRDBのデータ操作を行うための言語なので、RDBのデータ操作を行うのはRubyよりSQLのほうが得意であることは明らかです。Railsも「データ操作が複雑になったときは素直に生SQLを使え」というスタンスです。
ちなみにRails 6までは公式ドキュメントにArel
は登場しなかったと思うのですが、Rails 7では公式ドキュメントにArel
が登場します。ただし、「複雑なSQLはArel.sql
を使って生SQLを書いてね。」という内容なので、スタンスは変わっていないようです。Arel.sql
以外を使ってSQLを構築することは相変わらず非推奨だと思われます。
じゃあ、なぜArel
を使うのか。それは「しかたなく」です。生SQLをつかうと実行結果がActiveRecord::Relation
にならずscopeのチェーンができないことがあります。そんな場合でもいくらでもやりようはあるのですが(特にRails 7ではArel.sql
で全部解決すると思われる)、時には「Arel
を使ったほうが速く実装できる」「Arel
を使ったほうが既存のコードへの影響範囲が少ない」ということがあります。そんなときに「しかたなく」使います。
「しかたなく」使うものなので、使うときは以下の事項を覚悟しておいたほうがいいです。なんなら使うたびに心を痛めるべきでしょう。
- 内部APIなのでRails/Active Recordのバージョンアップで動かなくなることがある
- 内部APIなのでRails/Active Recordのバージョンアップで挙動が変更されても、パッチノートなどには書かれない
- 可読性が低くなりがち
上記のことを理解した上で「どうしてもArel
を使いたい場合」もしくは「遊びで触ってみたい場合」に以下を読み進めてみてください。
Arel
使用のサンプル
Arel
を使ったサンプルを作りました。特に断りがなければMySQLとPostgreSQL両方で動くはずです。MySQLとPostgreSQLで記述が異なる場合は両方を用意しました。MySQL/PostgreSQL以外のRDBMSは未確認です。
以下のバージョンで動作確認しています。
- activerecord (6.1.3)
および
- activerecord (7.1.2)
SQL関数の利用(DATE型の年で検索)
SQL関数を使うためにはArel::Nodes::NamedFunction
を使います。例としてDATE型を文字列に変換して、その値でWHEREをかけてみます。
Task.where( Arel::Nodes::NamedFunction.new( 'DATE_FORMAT', [ Task.arel_table[:created_at], Arel::Nodes.build_quoted('%Y') ] ).eq('2023') ) # => SELECT `tasks`.* FROM `tasks` WHERE DATE_FORMAT(`tasks`.`created_at`, '%Y') = '2023'
ちなみにSELECT句で使う場合はこんな感じです。
years = Task.select( Arel::Nodes::NamedFunction.new( 'DATE_FORMAT', [ Task.arel_table[:created_at], Arel::Nodes.build_quoted('%Y') ] ).as('year') ) # => SELECT DATE_FORMAT(`tasks`.`created_at`, '%Y') AS year FROM `tasks` years.first.year #=> "2023"
Task.where( Arel::Nodes::NamedFunction.new( 'TO_CHAR', [ Task.arel_table[:created_at], Arel::Nodes.build_quoted('YYYY') ] ).eq('2023') ) # => SELECT "tasks".* FROM "tasks" WHERE TO_CHAR("tasks"."created_at", 'YYYY') = '2023'
こちらもSELECT句で使う場合はこんな感じです。
years = Task.select( Arel::Nodes::NamedFunction.new( 'TO_CHAR', [ Task.arel_table[:created_at], Arel::Nodes.build_quoted('YYYY') ] ).as('year') ) # => SELECT TO_CHAR("tasks"."created_at", 'YYYY') AS year FROM "tasks" years.first.year #=> "2023"
JSON型(JSONB型)への問い合わせ
昨今、RDBで部分的にドキュメント指向っぽいことをしたくてJSON型(あるいはJSONB型)を使うことがあります。JSON型へのクエリは各RDBMSで独自に構文が定義されていることが多いです。それをArel
で使ってみます。これにはArel::Nodes::InfixOperation
を使います。
JSON 値を検索する関数はこちらを参照: MySQL :: MySQL 8.0 リファレンスマニュアル :: 12.18.3 JSON 値を検索する関数
ここでは->
演算子を使ってみます。
Task.where( Arel::Nodes::InfixOperation.new( '->', Task.arel_table[:contents], Arel::Nodes.build_quoted('$.meta') ).eq('foo') ) # => SELECT `tasks`.* FROM `tasks` WHERE `tasks`.`contents` -> '$.meta' = 'foo'
JSON関数と演算子はこちらを参照: 9.15. JSON関数と演算子
ここでは->>
演算子を使ってみます。
Task.where( Arel::Nodes::InfixOperation.new( '->>', Task.arel_table[:contents], Arel::Nodes.build_quoted('meta') ).eq('foo') ) # => SELECT "tasks".* FROM "tasks" WHERE "tasks"."contents" ->> 'meta' = 'foo'
特定のレコードを先頭にする
MySQLとPostgreSQLではORDER BY id = 123 DESC
などとすると、条件に一致するレコードを先頭に持ってくることができます。これをArel
で書いてみます。
Task.order(Arel::Nodes::InfixOperation.new('=', Task.arel_table[:id], 123).desc) # => SELECT `tasks`.* FROM `tasks` ORDER BY `tasks`.`id` = 123 DESC
おわりに
書き始める前はLEFT OUTER JOIN
やUNION
についても書くつもりでしたが、そういえばLEFT OUTER JOIN
はRails 6でArel
を使わずに書けるようになったんでした。UNION
もいつの間にかArel
を使わずに書けるようになってました。流石にUNION ALL
は無理だろ、と思ったらこれもArel
をつかわずに書けました(UNION
, UNION ALL
は内部APIっぽいメソッドを使う必要があるけど)。おかげで書く内容が少なくなってしまいました。
短かったですが、いかがだったでしょうか?ググってもすぐには見つからない、ちょっとマニアックな内容だったんじゃないでしょうか?ちなみにこの記事のタイトルについて。「むやみに使うものではない」という意味で「使いこなし術」です(後付)。
Arel
は積極的に使うべきものではないですが、非常時の対応として道具箱の中に入れておいても良いんじゃないかと思います。読者の方の開発に少しでも役立てれば幸いです。