- Ruby / Rails関連
Rails 7.2: ActiveRecord::Core#inspectの修正とattributes_for_inspectの便利な使い方(翻訳)
Rails 7.2: ActiveRecord::Core#inspectの修正とattributes_for_inspectの便利な使い方(翻訳)
Active Recordモデルのinspect
メソッドが返すのは、「モデルのクラス」と「すべての属性と値のリスト」です。Rails 7.2では、inspect
の出力にどの属性を含めてよいかを指定可能になりました。
本記事では、この機能を実装するきっかけとなったパフォーマンス問題について解説するとともに、この機能を活用して開発者のエクスペリエンスを快適にする方法についても説明します。
🔗 to_s
とinspect
について
Rubyのオブジェクトには、to_s
メソッドもinspect
メソッドも定義されます。to_s
はオブジェクトの文字列表現を返しますが、inspect
はデバッグに利用可能なオブジェクトの情報を返します。
Active Recordモデルではto_s
とinspect
の動作がどう違うかを見てみましょう。
Book.first.to_s #=> "#<Book:0x000000017765e6b8>"
Book.first.inspect #=> "#<Book id: 1, title: "The Rings of Saturn", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"
見ての通り、to
はモデルのクラス名だけを含むモデル名の短い文字列表現を返しますが、inspect
はモデルの属性とその値をすべて含む文字列を返します。
しかしこれは、inspect
呼び出しがto_s
呼び出しよりもずっと低速になってしまう可能性があるということです。inspect
を呼び出すときは、モデルの全属性をイテレーションして値を取得しなければならず、しかも、値の1つ1つをActiveRecord::ParameterFilter
で定義されているフィルタリストと照合して、いわゆる個人情報(Personal Identifiable Information: PII)が出力で露出しないように秘匿化する必要もあります。
つまり、inspect
の呼び出しコストは、モデルにある属性の個数とパラメータフィルタの個数に応じて増加します。一方、to_s
の呼び出しコストは一定です。
inspect
は一般にデバッグ目的で利用されますが、呼び出されるモデルによってはパフォーマンスが低下する可能性があるため、production環境でinspect
がどんなタイミングで呼び出されるかについて注意を払うべきです。しかし、今から説明するように、うっかりinspect
を呼び出して問題を引き起こすのは驚くほど簡単なのです。
🔗 inspect
は"フットガン"
Shopifyの開発者たちは、アプリケーションのパフォーマンスボトルネックを特定するために定期的にコードのプロファイリングを実施しています。昨年、あるチームが、Active Recordのinspect
メソッドの実行が特定のリクエストで8秒近くかかってしまっていることを発見しました。8秒はさすがに長すぎますね。
上の図はプロファイルから切り出したもので、Active Recordのinspect
メソッドの実行時間が7秒を超えていることを示しています。図の表示は左が最も値が大きいので、このリクエストではinspect
がトータル7秒以上を消費していることを意味します(消費時間は連続しているとは限りません)。このプロファイルについて詳しくはspeedscopeを参照してください。
さらに調べを進めると、このコードは配列に対してto_s
を呼び出していたのですが、その配列には数百個ものActive Recordモデルが含まれていることが判明しました。to_s
は、配列やハッシュなどのenumerable(列挙可能)なオブジェクトでは、単なるinspect
のエイリアスとして振る舞います(つまり、enumerableな要素をイテレーションして、要素ごとにinspect
を呼び出します)。
to_s
はさまざまな場面で呼び出される可能性があり、文字列の式展開でもto_s
が暗黙で呼び出されます。つまり、1個もしくは数百個のモデルに対してinspect
が呼び出される可能性があったとしても、すぐに見分けがつくとは限らないということです。
開発者は、この種の問題を「フットガン(footgun)」と呼ぶことがあります1。たしかにinspect
はデバッグで重宝しますが、使い方を間違えればproduction環境でパフォーマンス低下を簡単に引き起こしてしまう方法でもあるのです。Active Recordモデルがぎっしりつまった巨大配列に対してto_s
を呼び出すのはたいていの場合誤りの可能性が高いのですが、間違いはいつか起きるものであり、間違えたときにこれほどパフォーマンスを損なう問題を引き起こすべきではありません。
🔗 フットガンを「武装解除する」
アプリケーションのコードをリファクタリングして大量のinspect
呼び出しを回避すれば、この種のパフォーマンス低下の修正は一応可能です。しかし、このような悲惨な状況を回避するためには、Railsがproduction環境でもう少しうまく頑張って欲しいものです。
どんな解決方法が可能かを検討するときは、inspect
のユースケースがproduction環境に存在していることを思い出すのが重要です。したがって、私たちの解決方法は、単にproduction環境のinspect
メソッド呼び出しを再定義したり削除したりするだけでは終わりません。私たちの理想の解決方法は、inspect
のパワーを失わずに、意図しない誤用に対する安全性を提供することです。
いくつかの方法を検討した結果、以下の2つのソリューションに落ち着きました。
inspect
でどの属性を出力してよいかを開発者が設定可能にする- production環境では、
inspect
でid
のみを出力するようモデルを設定する
このソリューションについてもう少し詳しく見ていきましょう(#49765もチェックしてみてください)。
まず、ActiveRecord::Base
にattributes_for_inspect
という新しいクラス属性を作成しました。モデルのインスタンスでinspect
が呼び出されると、attributes_for_inspect
のリストに含まれている属性だけが出力に含まれるようになります。
Book.first.inspect #=> "#<Book id: 1, title: \"The Rings of Saturn\", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"
Book.attributes_for_inspect = [:id, :title]
Book.first.inspect #=> "#<Book id: 1, title: \"The Rings of Saturn\">"
次に、production環境ではすべてのモデルでattributes_for_inspect
の値を[:id]
に設定しました。これにより、production環境のRailsコンソールで以下の結果が表示されるようになります。
Book.first.inspect #=> "#<Book id: 1>"
これで、潜在的なパフォーマンス低下の問題は大幅に軽減されました。production環境では、モデルでinspect
を呼び出しても、:id
属性だけが表示されます。この場合、属性に対するパラメータフィルタも引き続きチェックされるので、to_s
を呼び出すよりは遅くなりますが、重要なのは、モデルの属性数が増えてもinspect
の呼び出しコストが増加しなくなったことです。
inspect
のフルパワーを取り戻す「脱出ハッチ」も2種類用意されています。
1つは、attributes_for_inspect
に:all
を渡すことで、これによって再びすべての属性が出力されるようになります。この振る舞いはdevelopmentモードとtestモードのデフォルトなので、従来通りinspect
をデバッグに利用可能になります。
Book.attributes_for_inspect = :all
Book.first.inspect #=> "#<Book id: 1, title: \"The Rings of Saturn\", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"
もう1つは、新しいfull_inspect
を呼び出す方法です。これは特定の場面でのみすべての属性を出力する必要が生じた場合に利用できます。
Book.attributes_for_inspect = [:id]
Book.first.inspect #=> "#<Book id: 1>
Book.first.full_inspect #=> "#<Book id: 1, title: "The Rings of Saturn", created_at: \"2024-03-19 10:16:20.229686000 -0400\", updated_at: \"2024-03-19 10:16:20.229686000 -0400\">"
🔗 ボーナス: attributes_for_inspect
で快適に開発しよう
attributes_for_inspect
を導入した動機は、もともとproduction環境でinspect
が使われた場合にパフォーマンスが低下する可能性のある問題を軽減することでした。しかしattributes_for_inspect
はアプリケーションの開発エクスペリエンスを快適にするのにも利用できるのです。
大規模なRailsアプリケーションでは、Active Recordモデルに何十個もの属性があり、場合によっては属性の値が非常に長くなることも珍しくありません。
inspect
は手動で呼び出して使うとは限りません。たとえば以下のようにRailsコンソールでオブジェクトが返されるときにもinspect
が自動で呼び出されますが、inspect
の表示量が増えると邪魔になることがあります。
myapp(dev)> Book.first
=>
#<Book:0x000000017f9a9d88 id: 1,
title: "The Rings of Saturn",
author: "W.G. Sebald",
published: 1998,
pages: 306,
language: "English",
type: "fiction",
format: "hardcover",
translated: true,
original_language: "German",
original_name: "Die Ringe des Saturn",
translator: "Michael Hulse",
isbn-13: 9780811213783,
isbn-10: "0811213781",
publisher: "New Directions",
created_at: "2024-03-19 10:16:20.229686000 -0400",
updated_at: "2024-03-19 10:16:20.229686000 -0400">
inspect
の出力量が増えた場合やターミナルウィンドウが狭い場合は、ある時点で上のように複数行に分割されます。ターミナルが出力結果でうずまってしまうと上下にスクロールしなければならなくなったり、デバッグセッションをいったん終了してからでないとコマンドもろくに入力できなくなったりすることがあります。inspect
の出力結果が多すぎると、テストが失敗したときの結果表示が崩れたりすることもあります。
おそらく皆さんも私と同様に、出力が多すぎて結果の解析で苦労したり、デバッグ中に画面に表示された大量の文字を理解するのに手間取ったりした経験がおありかと思います。私の場合、モデルの全属性について値を把握しなければならなくなることは、まずありません。むしろ、モデル属性の1つか2つについて値がわかれば、次の作業を決定するのに十分です。
そこで、attributes_for_inspect
を使えば、デバッグ時に最も有用な属性だけを出力できます。これは、特にモデルの配列を操作するときに便利で、数百行の出力をわずか数行に削減できます。
Rails 7.2のデフォルトでは、既存の振る舞いを維持するために、developmentモードとtestモードですべてのモデルに対してattributes_for_inspect
が:all
に設定されます。
しかし私としては、Railsコンソールを快適にするためにattributes_for_inspect
をぜひ皆さんにもお試しいただきたいと思います。full_inspect
を呼び出せばいつでもモデルの全属性を表示できることもお忘れなく。
🔗 まとめ
よく言われているように、Railsは切れ味のよい刃物も提供していますが、こうした強力な機能が思わぬ問題を引き起こすこともあります。
本記事では、開発者がうっかり大量のActive Recordモデルに対してinspect
メソッドを呼び出してパフォーマンス低下につながる可能性があることを見てきました。
Rails 7.2からは、inspect
で出力してよい属性をコンフィグで指定可能になりました。これは、inspect
が誤って呼び出されたときにパフォーマンスが低下する潜在的な問題を防ぐのに役立ちます。
さらに、この機能を活用すれば開発エクスペリエンスを快適にできることも見てきました。Railsアプリケーションを7.2にアップグレードする機会があったら、ぜひattributes_for_inspect
をお試しください。
関連記事
- footgunは「自分の足を撃ち抜くために作られたかのようなピストル」のことで、強力だが痛い目に遭う可能性がある機能というニュアンスで英語圏の技術記事でよく使われます。参考: Urban Dictionary: footgun ↩
概要
CC BY-NC-SA 4.0 Deedに基づいて翻訳・公開いたします。
CC BY-NC-SA 4.0 Deed | 表示 - 非営利 - 継承 4.0 国際 | Creative Commons
日本語タイトルは内容に即したものにしました。
参考: ウォッチ20231122:
ActiveRecord::Core#inspect
の出力をカスタマイズ可能になった