こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 production環境でraiseせずにレポートするErrorReporter#unexpectedを追加
前提条件違反をレポートする
ErrorReported#unexpectedを追加。例:
def edit if published? Rails.error.unexpected("[BUG] Attempting to edit a published article, that shouldn't be possible") return false end # ... end上のコードはdevelopment環境やtest環境ではエラーをraiseするが、production環境ではエラーのレポートだけを行う。
Jean Boussier
同Changelogより
つっつきボイス:「unexpectedはproduction環境ではエラーを出さずにレポートだけ出力するんですね」「エラーは開発段階のうちに検出したい」「似たようなコードを手書きすることは割りとあるので、Railsでサポートしてくれるのはよさそう👍」
# activesupport/lib/active_support/error_reporter.rb#L145
def unexpected(error, severity: :warning, context: {}, source: DEFAULT_SOURCE)
error = RuntimeError.new(error) if error.is_a?(String)
error.set_backtrace(caller(1)) if error.backtrace.nil?
if @debug_mode
raise UnexpectedError, "#{error.class.name}: #{error.message}", error.backtrace, cause: error
else
report(error, handled: true, severity: severity, context: context, source: source)
end
end
🔗 with_routingテストヘルパーを統合テストで使えるようにした
ActionDispatch::IntegrationTestにwith_routingテストヘルパーのサポートを追加する。
このヘルパーは、これまで統合テストのコンテキストで動かなかった(ActionController::TestCaseでしか使えなかった)。理由はテスト中にRackアプリと統合セッションが改変されなかったため。コントローラのテストはデフォルトで統合テストなので、この種のテストケースでもルーティングのテストをサポートすべき。動機/背景
このプルリクを作成した理由は、統合テストで
with_routingを使う必要があったが動かないらしかったため。詳細
このプルリクは、統合テストでのルーティングアサーションを変更して
ActionDispatch::Assertions::RoutingAssertions::WithIntegrationRoutingをincludeし、標準のwith_routing機構をオーバーライドして、テスト中のアプリと統合セッションを改変するようにする。追加情報
既存のルーティングアサーションテストについては共有テストモジュールを利用することで、コントローラのテストと統合テストを同じ方法で行えるようにした。
同PRより
つっつきボイス:「with_routingをこれまで統合テストで呼び出せなかったのを修正したのね」「with_routingというテストヘルパーがあったんですね」「ルーティングのテストを書く機会はそれほどないけど、必要ならば統合テストでも書くべきですね」
参考: Rails API with_routing -- ActionDispatch::Assertions::RoutingAssertions
参考: 統合テスト - Wikipedia
🔗 Active Storageでフォームの<button>要素内でのネストをサポート
動機/背景
この変更は、
type="submit"のbuttonまたはinputに子要素(spanやアイコンなどのHTML要素)が含まれる場合に発生する可能性のある問題に対処するために必要となる。現在のActive Storageでは、送信ボタンがクリックされたかどうかを判断するために
didClickイベントリスナーがクリックイベントのtargetをチェックしている。イベントのtargetプロパティは、クリックされた特定のHTML要素を参照する。送信ボタンに子要素が含まれており、さらに子要素の1つが実際にクリックされる要素である場合、
targetはボタンそのものではなく、この子要素を参照する。
didClick関数がチェックするのは、type="submit"のbuttonまたはinputであるかどうかなので、このチェックは失敗し、submitButtonsByFormにボタンが保存されなくなる。その結果、ダイレクトアップロードの後にフォームが送信されると、本来別のボタンがクリックされたとしても、フォーム内の最初の送信ボタンが誤ってフォーム送信に使われる可能性がある。複数の異なる送信ボタンがある場合、フォーム送信でそれぞれ異なるアクションをトリガーすることを目的としている可能性があるため、これは予期しない振る舞いとなる可能性がある。
詳細
クリックされた
type="submit"のbuttonまたはinput要素から最も近い先祖要素をElement.closest()メソッドで検索することで、クリックイベントがボタンの子要素によってトリガーされたときにもsubmitButtonsByFormに正しいボタンが保存されるようにした。これによって問題が解決し、ダイレクトアップロード後に正しいボタンでフォームを送信できるようになる。
同PRより
つっつきボイス:「Active Storageで使うJavaScriptコードの修正か」「要素がネストしている場合にどのボタンが押されたかを正しく検出するためにclosest()を明示的に使うようにしたということですね」「Active StorageにはUJS版のJavaScriptコードもあるんですね: UJS版もちゃんと更新されてサポートされているのは、レガシRailsアプリケーションにとってもありがたい👍」
# activestorage/app/assets/javascripts/activestorage.js#L756
function didClick(event) {
- const {target: target} = event;
- if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
- submitButtonsByForm.set(target.form, target);
+ const submitButton = event.target.closest(":is(button, input)[type='submit']");
+ if (submitButton && submitButton.form) {
+ submitButtonsByForm.set(submitButton.form, submitButton);
}
参考: Element: closest() メソッド - Web API | MDN
「コメントに具体例があった↓: こういうふうにネストしたボタンが複数ある場合がこれまで想定されていなかったということか」
<%= form_for @model do |form| %>
<%= form.file_field :image, direct_upload: true %>
<%= button_tag name: "save_method", value: "draft" do %>
<span>Save as draft</span>
<% end %>
<%= button_tag name: "save_method", value: "publish" do %>
<span>Save & Publish</span>
<% end %>
<% end %>
🔗 非推奨化された機能をRails 7.2に向けて削除
削除・非推奨化された機能
特記ないものは削除です。()内は非推奨化されたときのプルリクです。
- Action Mailer
- Action Pack
ActionDispatch::IllegalStateError定数(#47091)AbstractController::Helpers::MissingHelperError定数(#47199)ActionController::ParametersとHashの比較(#44866)allow_deprecated_parameters_hash_equalityコンフィグを非推奨化Rails.application.config.action_dispatch.return_only_request_media_type_on_content_typeコンフィグ(@689b277)- ブラウザのパーミッションポリシーの
speaker、vibrate、vrディレクティブ(#46199) Rails.application.config.action_dispatch.show_exceptionsへのtrue/falseの設定(#45867)
- Active Job
つっつきボイス:「Rails 7.1までに非推奨化された機能をRails 7.2で削除するプルリクです」「最終的に削除する機能はドラフトの#49624のChangelogにあるけど、今回はこの中からpart 1として選んだものを削除するということか」「Railsで振る舞いを削除するときは、非推奨にしたバージョンを経てから次のバージョンで削除することになっていますよね↓」「こういう作業は@rafaelfrancaが仕切ることが多いですね」
参考: § 5.11.1 振る舞いを削除する場合 -- Ruby on Rails に貢献する方法 - Railsガイド
🔗 MySQLクエリでwarningが発生してもActiveRecord.db_warnings_actionが呼び出されないケースを修正
MySQLが返す
warning_countがゼロより大きく、SHOW WARNINGSクエリを実行しても警告が表示されない場合に、ActiveRecord.db_warnings_actionを呼び出しても警告を無視せずに一般的な警告メッセージを表示するよう修正。Kevin McPhillips
同Changelogより
問題
ActiveRecord.db_warnings_actionが:ignore以外の値に設定されている場合、警告があったら「常に呼び出される」ことを期待する。しかしMySQLでは必ずしもそうなると限らない。
AbstractMysqlAdapterでは以下のように2つのステートメントで警告が処理されている。rails/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
return if ActiveRecord.db_warnings_action.nil? || @raw_connection.warning_count == 0 @affected_rows_before_warnings = @raw_connection.affected_rows result = @raw_connection.query("SHOW WARNINGS") result.each do |level, code, message|しかし単純化したロジックは以下のようになる。
query_result = raw_connection.query(sql) if raw_connection.warning_count != 0 warning_result = raw_connection.query("SHOW WARNINGS") warning_result.each do |warning| ActiveRecord.db_warnings_action(warning) end endつまりクエリは結果から
warning_countをメタデータとして返す。警告を取得するために2つめの"SHOW WARNINGS"クエリが実行されるが、これは常に正しいと見なされる。warning_countがゼロより大きいにもかかわらず[]を返す例が2つある。
- MySQLの
max_error_countがゼロに設定されていると、警告はカウントされるが決して永続化されないため、SHOW WARNINGSは常に空になる(典拠)。ProxySQLを使う場合、警告はまったく伝播されないらしい(ProxySQL does not propagate database warnings sysown/proxysql#1751を参照)。
この他にもあるかもしれない。
つまり振る舞いとしては、
db_warnings_actionが:raiseやprocなどに設定されていたとしても決して実行されず、警告も表示されない。解決方法
warning_countがゼロ以外なのにSHOW WARNINGSが[]を返す場合は、エラーメッセージに警告を追加する。このメッセージはとても有用というわけではないが、警告の発生を隠蔽するよりは良い。これで、ユーザーがDBコンフィグを更新したり、カスタムprocを追加したり、警告がユースケースで適切化どうかをトラッキングしたりできるようになる。また、
trilogyでは警告の振る舞いがまったくテストされていなかったことにも気づいたので、最初に警告のテストを追加した(これは基本的にmysql2のテストのコピーである)。疑問点
- エラーメッセージの言い回しはこれでいいだろうか?自分ではベストのつもりだが、こだわりはない。
db_warnings_actionに関するドキュメントを追加して、この状態に陥ることを防ぐDB調整方法のヒントとすべきだろうか?このプルリクでやるのが適切とは思えないのと、ドキュメントの他の場所でインフラ関連のヒントを示すのがいいかどうかわからない。内容が具体的すぎて陳腐化するかもしれない。
同PRより
つっつきボイス:「MySQLで警告を出して欲しいのに出されない場合があったのを修正したそうです」「max_error_countの設定を0にしている場合にのみ発生する問題のようだけど、知らないと気づくのが難しい問題ですね」
🔗 DBアダプタ名のエイリアスを利用可能にした
問題
DBアダプタ名のエイリアスを使えるようにしたい。このプルリクでの最終状態は、
mysqlをtrilogyやmysql2にマッピング可能にすること。他のDBアダプタについても同じことができるようにしたい。ここでの主なリファクタリングは、アダプタのバリデーションを
DatabaseConfig.newから切り出して、後で呼び出される#validateに移動すること。このリファクタリングの理由は、現在のデータベースコンフィグはフレームワークで非常に早期のタイミングで読み込まれるので、コンフィグが読み込まれてから
.newでバリデーションされる前にアダプタの登録やエイリアス化にフックする方法が存在していないため。rails/activerecord/lib/active_record/railtie.rb
initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do self.configurations = Rails.application.config.database_configurationアダプタがActive Recordに登録されるタイミングは、「アダプタが読み込まれた後」かつ「コンフィグが読み込まれる前」でなければならない。また、
on_load(:active_record)フックが順に実行されるため、上のコードは常にRailsアプリの他のもの(イニシャライザも含む)よりも前に実行される。:active_record_register_adaptersフックを追加しようとしたものの、これが起動される前にアプリのイニシャライザは実行されなかった。解決方法
@matthewdの提案に沿って、バリデーションを
DatabaseConfigのメソッドに切り出す。この方法なら、コンフィグのサブクラスでバリデーションをさらに特殊化できるというボーナスもついてくる。また、次のステップで使う予定のメソッドも
alias("trilogy", as: "mysql")に追加する。再現手順
この再現手順は、新規Railsアプリで"sqlite3"アダプタ名を"potato"にし、以下の手順でアプリを実行する形で検証した。
$ ../rails/railties/exe/rails new example --dev --skip-asset-pipeline --skip-javascript --skip-hotwire --skip-test$ sed -i 's/adapter: sqlite3/adapter: potato/' config/database.yml$ bin/rails environment bin/rails aborted! ActiveRecord::AdapterNotFound: database configuration specifies nonexistent 'potato' adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile. (ActiveRecord::AdapterNotFound)$ echo 'ActiveSupport.on_load(:active_record) { ActiveRecord::ConnectionAdapters.register("potato", "ActiveRecord::ConnectionAdapters::SQLite3Adapter", "active_record/connection_adapters/sqlite3_adapter") }' > config/initializers/active_record_adapters.rb$ bin/rails environment $ bin/rails runner 'puts "#{ ActiveRecord::Base.connection.pool.db_config.adapter } / #{ ActiveRecord::Base.connection.adapter_name }"' potato / SQLite同PRより
つっつきボイス:「mysqlアダプタにtrilogyやmysql2みたいなエイリアスを作れるようにしたかったんですね」「やりたいことはシンプルで既存の振る舞いも変わらないけど、初期化のタイミングなどとの関連でそれなりの規模の修正になってますね」
🔗 Active Storageのプロキシで画像のバリアントが返されない問題を修正
ActiveStorage::Representations::ProxyControllerが、previewableなファイルでプレビュー画像のバリアントを正しく返さない問題を修正。Chedli Bourguiba
同Changelogより
修正issue: #47071
動機/背景
blobが
previewableとして表示可能な場合、プロキシされるプレビュー画像が常に元のプレビュー画像になり、リクエストで渡されるvariation_keyが完全に捨てられてしまっていた。3つのクラスは100%互換性があるので、
Representable#imageの利用を非推奨にできる。このメソッドは既に古くなっているので、このクラスのユーザーが呼び出す必要はなく、Preview#imageの場合に呼び出すのは間違っている。詳細
このプルリクは、
PreviewとVariantWithRecordを編集してVariantAPIと完全に動機させることで問題を解決する。これによって、ProxyControllerがrepresentable#imageではなくrepresentableを呼び出せるようになる。追加情報
この問題を検出するため、これまでなかった「失敗するテスト」を追加した。
ActieStorage::VariantとActieStorage::VariantWithRecordとActieStorage::Previewに若干の食い違いがあったため、修正は簡単にはいかなかった。たとえば、
ActiveStorage::Variant#imageが(Active Storageのblobではなく)selfを返していたというだけの理由で、ActieStorage::Variantが(send_blob_streamで使われるあらゆるメソッドに加えて)#filenameなどの冗長なメソッドも大量に追加していた。以下は
@representation.imageを呼び出している行:rails/activestorage/app/controllers/active_storage/representations/proxy_controller.rb
http_cache_forever public: true do send_blob_stream @representation.image, disposition: params[:disposition] endこれを
send_blob_stream @representationを呼び出すように変更して#imageを削除した。これは今後のプルリクで非推奨化できる。
同PRより
つっつきボイス:「プレビュー画像のバリアントが返されなかったら悲しい」「これはバグですね: 7-0-stableにもバックポートされている」「同じ機能のクラスが3つもあったのを整理しながら修正したんですね」
前編は以上です。
バックナンバー(2023年度第4四半期)
週刊Railsウォッチ: Ruby 3.3.0-preview3リリース、IRBのオートコンプリート強化ほか(20231127後編)
- 20231122前編 Rails+Ruby 3.3でYJITがデフォルトでオンにほか
- 20231116後編 RBS 3.3.0とSteep 1.6.0リリース、Ruby Conf Taiwan 2023ほか
- 20231114前編 MariaDBのRETURNINGオプションをサポートほか
- 20231107 Kaigi on Rails発表「Simplicity on Rails」を見るほか
- 20231025後編 ShopifyのWebAssemblyツールチェインRuvyほか
- 20231024前編 7.1アップグレードガイドにActive Record暗号化設定の注意事項が追加ほか
- 20231018後編 Kaigi on Rails 2023関連イベント情報公開、複合主キーのlocality解説記事ほか
- 20231017前編 Active Storageのしくみを詳しく解説するDiscussion投稿ほか
- 20231011 Rails 7.1.0リリース、YARPがprismにリネームほか
- 20131004 Rails 7.1.0.rc1と7.1.0.rc2がリリース、SQLite3コンフィグの最適化ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)