pockestrap

Programmer's memo

activerecord-originator をリリースしました

こんにちは。 id:Pocke です。

今日は activerecord-originator という gem を作ったので紹介します。

github.com

なにこれ

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 にはmarginaliaactiverecord-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します(短いので全文載せてしまいます)。

https://github.com/pocke/activerecord-originator/blob/v0.1.0/lib/activerecord/originator/arel_node_extension.rb

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します。

https://github.com/pocke/activerecord-originator/blob/v0.1.0/lib/activerecord/originator/arel_visitor_extension.rb

ToSqlクラスは Arel の AST を渡り歩いて SQL 文字列を生成するクラスです。 ArelVisitorExtensionモジュールでは、ToSqlクラスの中の各ノードに対応するメソッドを上書きして、ArelNodeExtensionが記録した位置情報を元にコメントを差し込んでいます。

この2つのモジュールが実装の核となっています。 実装自体はあまり難しいものではないのですが、このように Rails の内部APIにべったり依存した形となってしまっています。


実行されるSQLをよりデバッグしやすくする gem である activerecord-originator の紹介でした。ぜひ試していただけると嬉しいです。


  1. この gem の発想を思いついたのは、default_scopeで定義された条件がそれと分からず、どこで定義されているのか悩んでいる人を見たことでした。