activerecord-originator をリリースしました
こんにちは。 id:Pocke です。
今日は activerecord-originator という gem を作ったので紹介します。
なにこれ
Active Record が発行するSQLの各部分に、それがどこで作られたものかをコメントとして入れ込む gem です。
理解するには実例を見るのが早いでしょう。次のログはArticlesController#index
で実行されるクエリの例です。
Article Load (0.1ms) SELECT "articles".* FROM "articles" WHERE "articles"."status" = ? /* app/models/article.rb:3:in `published' */ AND "articles"."category_id" = ? /* app/controllers/articles_controller.rb:3:in `index' */ ORDER BY "articles"."created_at" DESC /* app/models/article.rb:4:in `order_by_latest' */
/* ... */
で囲まれているコメントは、この gem によって追加されたものです。これを見ると "articles"."status" = ?
が article.rb
の3行目、published
メソッドから由来していることや、category_id
はコントローラーでフィルタされていること、ORDER BY
を定義しているscopeがどこにあるのかがわかります。
Motivation
先に挙げた簡単な例だとあまり嬉しさを感じないかもしれません。
ですが SQL の組み立てが複雑になると、この gem が真価を発揮するでしょう。scope の定義などでクエリの実行位置と組み立て位置が大きく離れていることはよくあります。また複雑な検索のためにクエリを組み立てるためのクラスなどを作っていると、そのクラスの中を探して回るだけでも大変です。この gem があれば、そのクエリがどのように組み立てられたかがピンポイントで分かるため、デバッグが容易になります。
他にもdefault_scope
のような、どこで付与されたのか分かりづらい条件をデバッグする際にも役立つでしょう。1
Usage
次のコマンドを実行して、activerecord-originator
gem をインストールします。
$ bundle add activerecord-originator
セットアップは以上で終わりです。gem をインストールするだけで出力されるクエリにコメントが付与されます。
Alternative Solutions
類似の gem にはmarginaliaやactiverecord-causeがあります。また Rails 7からは marginalia と同様の機能が Rails に標準で搭載されています。
これら従来の機能と activerecord-originator との大きな差は、コメント(やログ)を出力する単位です。従来の機能ではコメントの出力はクエリ単位です。そのクエリがどこで実行されたか知ることができるのはとても便利ですが、より細かい情報を得ることはできませんでした。
activerecord-originator ではコメントを SQL の各構文要素単位で出力することで、より細かい情報を伝えることができます。今まで分からなかった情報が分かるようになり、デバッグする際の情報がより増えます。
なお activerecord-originator は従来の機能を置き換えるものではなく、併用することを意図しています。
Caution
activerecord-originator を使う上で注意したいことが2つあります。1つは Active Record の内部APIが使われていること、もう1つはパフォーマンスです。
Active Record Internal API
この gem は Active Record の内部APIである Arel に強く依存しています。そのため将来的な Rails のアップデートで容易に壊れる可能性があります。 たとえば内部の大きなリファクタリングなどがあると壊れてしまうでしょう。
また古い Rails のバージョンではうまく動かない可能性があります。一応 Active Record v6.0 で一部を除きテストが通ること、 6.1 でテストがすべて通ることは確認していますが、今後も古いバージョンをサポートし続けられる保証はありません。
簡単に付け外しできる gem であるため、Rails のアップデートの際にはGemfile
から消す必要があるかもしれない、ぐらいの気持ちで向き合うと良いと思います。
Performance
コメントを出力するための処理がクエリを組み立てるたびに行われるため、そこそこのコストがかかることが予想されます。 そのため本番環境などで有効にするとパフォーマンスに影響が出ることが考えられます。
とはいえベンチマークを取るなどはまだ何もしていないため、実際どの程度影響があるのかはわかりません。 実際にどの程度影響が出るのか計測した場合は、私まで教えていただけると嬉しいです。
Implementation
最後に実装を簡単に紹介します。
まずこの gem はすべてのArel::Nodes::Node
の子孫クラスにAcitveRecord::Originator::ArelNodeExtension
モジュールをprepend
します(短いので全文載せてしまいます)。
module ActiveRecord module Originator module ArelNodeExtension def initialize(...) __skip__ = super @ar_originator_backtrace = caller end attr_reader :ar_originator_backtrace end end end
このモジュールによって、すべてのノードはそれが作成された位置(== where
メソッドなどが呼び出された位置)を記録します。
そしてもう1つActiveRecord::Originator::ArelVisitorExtension
モジュールをArel::Visitors::ToSql
クラスにprepend
します。
ToSql
クラスは Arel の AST を渡り歩いて SQL 文字列を生成するクラスです。
ArelVisitorExtension
モジュールでは、ToSql
クラスの中の各ノードに対応するメソッドを上書きして、ArelNodeExtension
が記録した位置情報を元にコメントを差し込んでいます。
この2つのモジュールが実装の核となっています。 実装自体はあまり難しいものではないのですが、このように Rails の内部APIにべったり依存した形となってしまっています。
実行されるSQLをよりデバッグしやすくする gem である activerecord-originator の紹介でした。ぜひ試していただけると嬉しいです。
-
この gem の発想を思いついたのは、
default_scope
で定義された条件がそれと分からず、どこで定義されているのか悩んでいる人を見たことでした。↩