ActiveRecordとArelで複雑なクエリを書く

グローバルソリューション事業部の平田です。

今回はActiveRecordの内部で利用されているライブラリ、Arelについてのtipsです。

Arelとは?

Arel (A Relational Algebra) とはSQLの抽象構文木 (AST; abstract syntax tree) を取り扱うためのライブラリです。乱暴な言い方をすればRubyの文法でSQL文を組み立てるためのライブラリです。(ActiveRecordより低レベルに位置し、ActiveRecordは内部的にArelを利用してSQL文を組み立てています。)

シンプルな問い合わせであればActiveRecordのみで事足りますが、問い合わせが複雑になるとSQL文 (の一部) を直接書くか、Arelを利用する必要があります。今回の記事では事例を挙げて、Arelで少しだけ複雑な問い合わせを行うまでを解説します。

事例

以下のような2つのモデルがあり、契約の期限日から一定の日数前に期限切れのお知らせメールを送付することを考えます。「何日前に送付するか」という情報はサービスごとに設定 (Service#expiry_notification_days) されています。メール送信の対象となる契約は、「Contract#expires_on からこの日数を引いた日付が今日以前で、メールが未送信 (Contract#expiry_notification_sent_atがnil)」となります。

モデル

クラス

※ここで利用しているモデルは当社のドメイン名・SSL証明書の契約管理システムをベースにしていますが、説明のため単純化してあります。(実際のシステムでは、期限切れまでに何度かメール送信する必要があるため、日数は別のモデルに格納されています。)

Arelを利用したSQL文の組み立て

列名

SQLの列名 (正確にはそれを抽象化した Arel::Attributes::Attribute のインスタンス) は、クラス名.arel_table[列名] で取得することが出来ます。たとえば、Contract の expiry_notification_sent_at は、

となります。

SQL関数

ここでは、MySQLの DATEDIFF, CURDATE 関数を呼び出すことで日付の演算を行います。

CURDATE … 今日の日付を返します。

DATEDIFF … 日付の差を計算します。

これらの関数はArel::Nodes::NamedFunctionを利用して呼び出すことが出来ます。たとえば、CURDATEであれば、

となります。(CURDATEには引数が無いため、空のArrayを渡しています。)

比較演算

Arel::Attributes::Attribute や Arel::Nodes::NamedFunction など、Arel::Predications を継承したクラスは、eq, lteq などの比較演算が利用可能です。

たとえば、以下のように書きます。(nil と比較した場合、”IS NULL”, “IS NOT NULL” に変換されます。)

作成した問い合わせ

以上を踏まえて、Arelで問い合わせを書くと以下のようになります。

これは最終的に次のようなSQLのクエリとなります。

スコープ化

実際にはモデルのクラス内にスコープとして定義すると良いでしょう。(Contract.expiry_notification_targets でアクセス出来るようになります。)

まとめ

今回は、ActiveRecordからArel経由でSQL関数を呼び出す方法について説明しました。

Arelを利用することで直接SQL文を書くことがなくSQLの高度な機能を利用することができますが、複雑な問い合わせを行う場合にはかえって読みづらくなりがちです。このため、制限 (mergeができない、default_scopeが無視される) に留意しつつ、find_by_sqlやselect_allを使用して直接SQL文を書いてしまうことも選択肢として考えるべきでしょう。

Share on LinkedIn
LINEで送る
Pocket

平田

平田

本業はインフラエンジニアだけど、最近はプログラミングやっています。