こんにちは、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
を編集してVariant
APIと完全に動機させることで問題を解決する。これによって、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ウォッチタグ)