- Ruby / Rails関連
週刊Railsウォッチ: Action Mailerプレビューで全メールヘッダーを表示可能に、rubocop-graphqlほか(20230307前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
また引き離されてしまいましたのでピッチを上げます。
- 公式更新情報: Ruby on Rails — Parameter filtering and an improved ActionView::Helpers::TagHelper#token_list
- 公式更新情報: Ruby on Rails — This Week in Rails: preloading associations with composite keys and more!
🔗 Action ControllerのInstrumentation呼び出しでfiltered_path
を使うよう変更
機密性の高いクエリパラメータをフィルタするために
ActionController::Instrumentation
を変更した。イベントペイロードでfullpath
ではなくfiltered_path
を渡す。get "/posts?password=test" request.full_path # => "/posts?password=test" response.filtered_path # => "/posts?password=[FILTERED]"
Ritikesh G
同Changelogより
参考: §3.1 Action Controller -- Active Support の Instrumentation 機能 - Railsガイド
つっつきボイス:「Instrumentationイベント内で#full_path
が使われていたことで、GET渡しのクエリパラメータなどが適切にフィルタされないままイベントが送信されていたのをフィルタするようにしたんですね」「filtered_path
はログでパスワードとかを[FILTERED]
でマスクした文字列を取り出すメソッドですね」「こういったイベントデータのカスタマイズは自分でInstrumentation呼び出しのコードを書けばできますが、InstrumentationからのイベントはAPM(アプリケーションパフォーマンス監視)サービスのデフォルト設定で利用されることも多いので、知らない間に重要な情報がAPMサービスに流れていたということにならないようにデフォルトでフィルタ有効化されている方が安心ですね👍」
参考: §3.2.21 config.filter_parameters
-- Rails アプリケーションを設定する - Railsガイド
🔗 token_list
ヘルパーでStimulusのdata-action
を余分なエスケープから保護する
動機/背景
このコミット以前は、token_list
ヘルパー呼び出しにdata-action
属性値を渡すとHTMLエスケープが1個多すぎてしまう可能性がある。以後呼び出しを繰り返すと問題がさらに積み上がってしまう。
たとえば、以下の呼び出しをパースするとHTMLエスケープが多すぎてディスクリプタが無効になる可能性がある。first = "click->controller#action1" second = "click->controller#action2" third = "click->controller#action3" fourth = "click->controller#action4" value = token_list(first, token_list(second, token_list(third))) CGI.unescape_html value.to_s # => "click->controller#action1 click->controller#action2 click->controller#action3 click->controller#action4"
詳細
CGI.unescape_html
を用いることで、token_list
(値を再エスケープする)に渡される前の各String
値をロスなしに連結処理し、HTML安全性も維持できるようになる。このコミット後、上の例は以下のように期待通り動作する。
first = "click->controller#action1" second = "click->controller#action2" third = "click->controller#action3" fourth = "click->controller#action4" value = token_list(first, token_list(second, token_list(third))) CGI.unescape_html value.to_s # => "click->controller#action1 click->controller#action2 click->controller#action3 click->controller#action4"
同PRより
つっつきボイス:「Stimulusサポートの修正です」「token_list
ヘルパーでHTMLの>
->>
みたいなエスケープが重複発生していたのね」「RubyのCGI.unescape_html
で修正されている」
# actionview/lib/action_view/helpers/tag_helper.rb#L365
def token_list(*args)
- tokens = build_tag_values(*args).flat_map { |value| value.to_s.split(/\s+/) }.uniq
+ tokens = build_tag_values(*args).flat_map { |value| CGI.unescape_html(value.to_s).split(/\s+/) }.uniq
safe_join(tokens, " ")
end
参考: Rails API token_list
-- ActionView::Helpers::TagHelper
参考: CGI.unescapeHTML
(Ruby 3.2 リファレンスマニュアル)
🔗 メーラーのプレビューで全ヘッダーを表示する機能が追加
- PR: Added option to show all headers for mailer previews by swanson · Pull Request #47317 · rails/rails
動機/背景
メールに追加したカスタムヘッダーをプレビューする手軽な方法がなかった。メールスレッド用のMessage-ID
ヘッダーや、メールサービスプロバイダ固有の分析用タグやアカウントメタデータといったヘッダーはproductionのRailsアプリケーションでしばしば用いられる。mailcatcherやmailhogが提供しているような「フェイク受信トレイ」でメールの全ヘッダーを手軽に調べられるようにする。
詳細
このプルリクは、メーラーのプレビューテンプレートに展開可能なセクションを追加する。このセクションを展開すると、そのメーラーで全ヘッダーの表が表示される。
つっつきボイス:「お、Action Mailerの標準機能でメールヘッダーを全部表示できるのは便利👍」「これはありがたい🙏」「開発でメールヘッダーを全部見たいことは普通にありますね」
参考: § 2.6 メールのプレビュー -- Action Mailer の基礎 - Railsガイド
🔗 Action Cableのログ出力でパラメータをフィルタするようにした
つっつきボイス:「上の#47296はActionController内でのInstrumentationイベント内文字列をフィルタするようにしたけれど、こちらはAction Cableのログが対象か」「今回はフィルタ関連の改修が続きますね」「config.filter_parameters
での設定がAction Cableにも効くようになるのね」
# actioncable/test/channel/base_test.rb#L108
+ test "does not log filtered parameters" do
+ @connection.server.config.filter_parameters << :password
+ data = { password: "password", foo: "foo" }
+
+ assert_logged(':password=>"[FILTERED]"') do
+ @channel.perform_action data
+ end
+ end
参考: §3.2.21 config.filter_parameters
-- Rails アプリケーションを設定する - Railsガイド
🔗 Associations::Preloader
で複合外部キーの関連付けをプリロード可能になった
このプルリクは、
Associations::Preloader
で複合外部キーをquery_constraints
によって関連付けられている関連付けをプリロードできるようにし、暗黙でincludes()
リレーションをサポートするようにする。
複数テナントのレコードをバルクでクエリする場合の基本的な要件は、カラム名と値のセット/タプルを指定可能であること。最も効率の良いSQL句では以下のように行コンストラクタを利用する。select * from comments where (blog_id, blog_post_id) IN ((1,2), (1,3))
しかし現在のRailsでは行コンストラクタ句をサポートしていないので、以下のような
OR
句を連続で使うことになる。select * from comments where (blog_id=1 and blog_post_id=2) OR (blog_id=1 and blog_post_id=3)
この方法は、クエリに含まれる値の個数に応じてクエリが急速に肥大化するため効率があまりよくないが、有効な方法ではある。
テナントベースのアプリケーションでは、ほとんどのクエリが単一テナントを対象として実行されることを想定しているが(上述のコード行)、Rails(あるいはArel?)は上のクエリを以下のように賢く変換してくれる。
select * from comments where blog_id=1 AND (blog_post_id=2 OR blog_post_id=3)
最適化の可能性
このプルリクで最適化を行うのは適切ではないと考えるが、上のクエリはさらに以下のようにSQL句をできるだけ短くする形での最適化が見込まれる。
select * from comments where blog_id=1 AND blog_post_id IN (2,3)
このオプションについてまだ調べてはいないが、可能なら
load_records_for_keys
レベルまたはArel内部で実装されることを期待したい。さらなるステップ
理想としては、Railsが最終的に行コンストラクタをサポートし、
load_records_for_keys
を変更せずにresulting_scope.where(association_key_name => keys)
(association_key_name
は単一カラムまたはカラムセットを参照する)のようにシンプルにできるようにするのが望ましい。
同PRより
参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 8.2.1.22 行コンストラクタ式の最適化
つっつきボイス:「Railsで複合主キーが使えるようになったのかな?この改修は複合外部キーが対象だけど、複合主キーへの対応も順調だといいな」「複合外部キーは#47230がmainブランチにマージされていたんですね」「複合主キーといえばcomposite_primary_keys gemが定番ですね↓(ウォッチ20201209): DB設計をしていれば複合主キーは普通に使いたい」「サロゲートキーにしたくないこともありますよね」
参考: Rails API create_table
-- ActiveRecord::ConnectionAdapters::SchemaStatements
:primary_key
主キーが自動的に追加される場合の主キーの名前。デフォルトはid
。このオプションは:id
をfalseにすると無視される。
配列を渡すと複合主キーが作成される。
Active Recordモデルはこれらの主キーを自動検出することに注意。これはモデルでself.primary_key=
を用いて明示的に主キーを定義することで回避できる。
APIドキュメントcreate_table
--ActiveRecord::ConnectionAdapters::SchemaStatements
より
つっつき後にRedditの書き込みをきっかけに調べてみると、APIドキュメントには上のように書かれていますが、#29135を見ると上のAPIドキュメントにある複合主キーの作成はマイグレーションレベルの話のようで、複合主キーはActive Recordではまだ公式にサポートされていないようです↓。
# https://github.com/rails/rails/blob/09f5d816a2b7a7828eca58c0c94682ae63d294bf/activerecord/lib/active_record/attribute_methods/primary_key.rb#L137 private def suppress_composite_primary_key(pk) return pk unless pk.is_a?(Array) warn <<~WARNING WARNING: Active Record does not support composite primary key. #{table_name} has composite primary key. Composite primary key is ignored. WARNING end
🔗 SchemaCache#init_with
で重複解消をスキップ可能になった
- PR: SchemaCache#init_with skip deduplicate if specified by byroot · Pull Request #47395 · rails/rails
これは
SchemaCache
のシリアライザを使っている人向けの非常に高度なAPI。カスタムシリアライザでは既にデータを重複解消(deduplicate)している可能性があり、その場合に再度重複解除するのは非常に無駄が多い。
同PRより
参考: Rails API ActiveRecord::ConnectionAdapters::SchemaCache
つっつきボイス:「deduplicateが不要な場合にスキップできるようにしたんですね: very advanced APIとあるので使う人は限られそう」「デデュプリケートって言いにくい...」
# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L68
def init_with(coder)
@columns = coder["columns"]
+ @columns_hash = coder["columns_hash"]
@primary_keys = coder["primary_keys"]
@data_sources = coder["data_sources"]
@indexes = coder["indexes"] || {}
@version = coder["version"]
@database_version = coder["database_version"]
- derive_columns_hash_and_deduplicate_values
+ unless coder["deduplicated"]
+ derive_columns_hash_and_deduplicate_values
+ end
end
🔗 Dockerfieのユーザー名をappuser
からrails
に変更
些細ではあるが、他のDockerイメージと衝突する可能性のある一般的な
appuser
ではなく、rails
イメージの中にいることをコンテナユーザーが認識できるようになる。
同PRより# railties/lib/rails/generators/rails/app/templates/Dockerfile.tt#L69 # add custom user -RUN useradd appuser +RUN useradd rails -USER appuser:appuser +USER rails:rails # Copy built artifacts: gems, application -COPY --from=build --chown=appuser:appuser /usr/local/bundle /usr/local/bundle -COPY --from=build --chown=appuser:appuser /rails /rails +COPY --from=build --chown=rails:rails /usr/local/bundle /usr/local/bundle +COPY --from=build --chown=rails:rails /rails /rails
つっつきボイス:「Docker関連の改修も入りました」「ところで、Railsコンテナの名前に使うのはrails
以外にapp
なんかも考えられるけど、少なくともappuser
じゃない方がいいですね」「たしかに」
🔗Rails
🔗 Railsにモンキーパッチを当てるのは避けよう(Ruby Weeklyより)
つっつきボイス:「Shopifyの記事です」「モンキーパッチはRailsのアップグレードのときに問題になりやすいので極力当てたくないけど、実装上の都合でやむを得ず当てざるを得ないこともあるんですよ」「gemがモンキーパッチを当ててくることもごくまれにありましたね」「イニシャライザとかでモンキーパッチを当てた当時は覚えていても、すぐ忘れられがち」「Rubyのrefinementである程度は緩和できますけどね↓」
「Shopifyも以下の自社製gemでActive Recordのアダプタにモンキーパッチを当てていたけど、その後本家にプルリク#46690を投げてマージしてもらったそうです↓」「手元でモンキーパッチで回避するよりはアップストリームにマージしてもらう方がいいですよね」
- PR: Allow SQL warnings to be reported. by adrianna-chang-shopify · Pull Request #46690 · rails/rails
「Railsのmainブランチにしか入っていない機能や修正がどうしても必要なときとかに、やむを得ずモンキーパッチを当てることはあるかな」「やるとしたらそうかも」
「記事では、モンキーパッチより先にRailsをアップグレードする方を検討しようと書かれてますね」「たしかにそうなんですけどね...」「でも使っているgemによってはアップグレードが難しくなったりすることもあるんですよ」「う〜む」
🔗 RSpecではcontext間の違いを表現するときにのみlet
を使う
つっつきボイス:「onkさんが以下の翻訳の元記事に応答した記事です」
「タイトルを見ただけで伝えたいことがよくわかる記事ですね: 特定のcontextの中でのみ切り替えたい変数がある場合に"だけ"let
を使いたいというのはとても理解できる👍」「ですよね」「どんなlet
がよくないかは、let
を使っていくうちに体得していく形になるかな」
参考: RSpecドキュメント let
-- Module: RSpec::Core::MemoizedHelpers::ClassMethods
— Documentation by YARD 0.9.28
「let
は乱用して欲しくないけど、特定のcontextの中でだけ変数を上書きするのはlet
じゃないと書きにくかったりする」「そうそう」「その書き方ならテストコードの近くにlet
が置かれるので、どの変数が上書きされているかが見えやすくなります」「なるほど」「そういえば元記事のリファクタリング後のテストコードではlet
が末尾に固められていましたね↓」
# https://techracho.bpsinc.jp/hachi8833/2023_02_13/126094より
...
let(:user) { create(:user) }
let(:teacher) { create(:user, :teacher) }
let(:student) { create(:student, parent: parent) }
let(:parent) { create(:parent) }
let(:new_zip_code) { Faker::Address.zip_code }
let(:parent_first_name) { Fake::Name.female_first_name }
end
🔗 Railsのログを最大限活用する
つっつきボイス:「AppSignalの記事です」「記事はログの出力レベルなどのRailsのログに関する定番の話かな: AppSignalはパフォーマンス監視サービスなどを手がけているのでRailsのログは同社の得意な分野ですね👍」
参考: §2.2 ログの出力レベル -- Rails アプリケーションのデバッグ - Railsガイド
「記事にもあるように、ログをいかに読みやすく出力するかは大事: 呼び出し元クラスのような情報がログにないと追いかけるのが大変」
# 同記事より
def call_external_api(user_id, payload)
Rails.logger.debug('Calling external api')
client = Client.new(user_id)
response = client.request(payload)
if response.ok?
Rails.logger.info { "Request success, received #{response.body}" }
return response
else
Rails.logger.warn { "Request returned #{response.code}, #{response.body}" }
return response
end
rescue ClientError => e
logger.error { "#{e}: #{e.message}" }
end
「ある程度以上読みやすくしようと思ったら、記事にもあるようにstructured logにすることになるでしょうね」「記事ではLogstashサービスやlograge gemが紹介されていますね」
# 同記事より
# lib/json_log_formatter.rb
class JsonLogFormatter < ::Logger::Formatter
def call(severity, time, progname, msg)
json = { time: time, progname: progname, severity: severity, message: msg2str(msg) }
.compact_blank
.to_json
"#{json}\n"
end
end
# application.rb
config.log_formatter = JsonLogFormatter.new
参考: Logstash:データの一元化、変換、保管 | Elastic
記事では他にもougaiやmr-loga-logaというカスタムロガーも紹介されていました↓。
🔗 rubocop-graphql(RubyFlowより)
つっつきボイス:「rubocop-graphqlが出ていたのを初めて知りました」「これはgraphql-rubyを使ったRubyコードを対象とするcopですね」「最初のコミットは2020年5月か」「graphql-rubyはかなり使われているはずだけど、そこまで世間的な書き方のベストプラクティスが確立している感じでもないので、対応するcopが提供されるのはその辺りを考えるきっかけになってよさそう👍」
参考: Module: RuboCop::Cop::GraphQL — Documentation for rubocop-graphql (1.0.0)
# https://www.rubydoc.info/gems/rubocop-graphql/RuboCop/Cop/GraphQL/ArgumentDescription
# フィールドごとにdescriptionがあること
# good
class BanUser < BaseMutation
argument :uuid, ID, required: true, description: "UUID of the user to ban"
end
# bad
class BanUser < BaseMutation
argument :uuid, ID, required: true
end
🔗 Railsアプリでメモリ肥大化を事前に発見する(Ruby Weeklyより)
つっつきボイス:「メモリ肥大化を発見するのは事前にあたりを付けておかないと難しいでしょうね: Railsで割とあるのはActive Recordで全レコードをメモリに乗せてしまうとかかな」「Active Recordでmap
を使ったりすると起きがち」「それそれ」
「記事ではio_monitor gemで調べる方法が紹介されていますね↓」「このコンフィグで思い出したけど、巨大なファイルをガバっと取得したりするとRubyのNet::HTTP
でメモリ肥大化につながる可能性はありそう」「最近だとスマホのカメラの解像度が高くなっていて画像の容量がでかいので、アップロードサービスを追加するときは以前よりもメモリ肥大化に注意が必要でしょうね」「Redisも巨大なデータをインデックス化して一括で取り出したりすると起きやすそう」
# 同記事より
IoMonitor.configure do |config|
config.publish = [:logs, :notifications, :prometheus] # defaults to :logs
config.warn_threshold = 0.8 # defaults to 0
config.adapters = [:active_record, :net_http, :redis] # defaults to [:active_record]
end
参考: library net/http (Ruby 3.2 リファレンスマニュアル)
参考: Redis | redis.io
🔗 その他Rails
書きました
Railsチュートリアルのsample_appに型を導入|ふーが https://t.co/FAwywx66uy #zenn— ふーが🌶🍜🥟 (@fugakkbn) February 22, 2023
「お、Railsチュートリアルに型を付けてみたのね」「静的型チェッカーはSteepを使ってますね」「SteepのVSCode拡張も使ってる」
参考: Ruby on Rails チュートリアル:プロダクト開発の0→1を学ぼう
参考: Steep - Visual Studio Marketplace
前編は以上です。
バックナンバー(2023年度第1四半期)
週刊Railsウォッチ: Ruby 3.2のData#initializeの設計、ruby-openai gemほか(20230222後編)
- 20230221前編 Ruby30周年記念イベント、ActiveRecord APIクイズほか
- 20230215後編 Bundler 2.4リリース、RubyKaigi 2023参加募集開始ほか
- 20230214前編 AssumeSSLミドルウェア追加、Fly.ioとRails 7.1のDocker対応ほか
- 20230202後編 ShopifyのYJIT記事、RubyGemsのgem execコマンドほか
- 20230201後編 Ruby 3.2のベンチマーク記事、dry-cliで高度なCLIを作るほか
- 20230131前編 Evil Martiansが使っているgem、JavaScriptガイドが更新ほか
- 20230125前編 2022年のRails振り返り記事、RailsにDocker関連ファイルが追加ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)