for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

Active Record Arel使いこなし術

こんにちは。フォースタートアップス株式会社、エンジニアの石田です。

前々回前回Rubyの話をしました。今回もRubyの話です。Active RecordのArelを使い尽くす、という内容です。お付き合いください。

注意

この記事は「Arelを使わないといけなくなったときに参考になるように」という目的で書きました。「Arelを積極的に使っていこう」という趣旨ではないのでご留意ください。詳しくは次章にて。

そもそもArelを使うことについて

ArelRailsの内部APIです。内部APIであるがゆえにRailsユーザーがこれを使うことは公式で推奨されていません*1 *2SQLRDBのデータ操作を行うための言語なので、RDBのデータ操作を行うのはRubyよりSQLのほうが得意であることは明らかです。Railsも「データ操作が複雑になったときは素直に生SQLを使え」というスタンスです。

ちなみにRails 6までは公式ドキュメントにArelは登場しなかったと思うのですが、Rails 7では公式ドキュメントにArelが登場します。ただし、「複雑なSQLArel.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を使ったサンプルを作りました。特に断りがなければMySQLPostgreSQL両方で動くはずです。MySQLPostgreSQLで記述が異なる場合は両方を用意しました。MySQL/PostgreSQL以外のRDBMSは未確認です。

以下のバージョンで動作確認しています。

および

SQL関数の利用(DATE型の年で検索)

SQL関数を使うためにはArel::Nodes::NamedFunctionを使います。例としてDATE型を文字列に変換して、その値でWHEREをかけてみます。

MySQL

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"

PostgreSQL

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を使います。

MySQL

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'

PostgreSQL

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'

特定のレコードを先頭にする

MySQLPostgreSQLでは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 JOINUNIONについても書くつもりでしたが、そういえばLEFT OUTER JOINRails 6でArelを使わずに書けるようになったんでした。UNIONもいつの間にかArelを使わずに書けるようになってました。流石にUNION ALLは無理だろ、と思ったらこれもArelをつかわずに書けました(UNION, UNION ALLは内部APIっぽいメソッドを使う必要があるけど)。おかげで書く内容が少なくなってしまいました。

短かったですが、いかがだったでしょうか?ググってもすぐには見つからない、ちょっとマニアックな内容だったんじゃないでしょうか?ちなみにこの記事のタイトルについて。「むやみに使うものではない」という意味で「使いこなし術」です(後付)。

Arelは積極的に使うべきものではないですが、非常時の対応として道具箱の中に入れておいても良いんじゃないかと思います。読者の方の開発に少しでも役立てれば幸いです。

参考文献