Ruby on Rails PR Digest - 2026年 5月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#57271 Gracefully handle nil during multi-parameter assignment
マージ日: 2026/5/5 | 作成者: @seanpdoyle
- 概要 (1-2文で)
マルチパラメータ属性(例:Dateカラム)の代入時に、これまで""のときだけ正常に「値なし」として扱われていた挙動を、nilに対しても例外なく同様に扱えるようにした修正です。nilを渡した際に発生していたNoMethodError: undefined method 'empty?' for nilを解消します。
- 変更内容の詳細
何が問題だったか
Rails のマルチパラメータ属性("last_read(1i)", "last_read(2i)", "last_read(3i)" のような形式のキー)に対して、値として空文字列 "" を渡すと「値なし」として扱われていました:
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
"last_read(1i)" => "",
"last_read(2i)" => "",
"last_read(3i)" => "",
}
topic.last_read # => nil一方で、同じ状況で "" を nil に置き換えると、内部実装で value.empty? のようなチェックをしていたため、NilClass に empty? が実装されておらず NoMethodError が発生していました:
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
"last_read(1i)" => nil,
"last_read(2i)" => nil,
"last_read(3i)" => nil,
}
# => NoMethodError: undefined method `empty?' for nil:NilClass何をどう直したか
activerecord/lib/active_record/attribute_assignment.rb のマルチパラメータ処理部分で、
- これまで:
value.empty?で「空」を判定していた - これから:
value.blank?で判定する
という変更が行われました(実質 1 行の差し替え)。
blank? は Rails 拡張であり、
String#blank?は内部でempty?などを使って空文字・空白文字列を判定NilClass#blank?も定義されていてtrueを返す
ため、"" だけでなく nil に対しても同じように「値なし」という扱いができます。
この修正後は次のように動作します:
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
"last_read(1i)" => nil,
"last_read(2i)" => nil,
"last_read(3i)" => nil,
}
topic.last_read # => nil (例外は発生しない)テストとドキュメント
activerecord/test/cases/multiparameter_attributes_test.rbに、nilを渡したケースが正しくnilに設定され、例外が出ないことを確認するテストが追加されています。activerecord/CHANGELOG.mdに、この挙動変更(バグフィックス)が追記されています。
- 影響範囲・注意点
対象となる機能
- Active Record のマルチパラメータ属性(
"column(1i)","column(2i)","column(3i)"など)を使った代入ロジック全般が対象です。典型的にはフォームからのDate/Time/DateTime入力のパラメータ処理で利用されます。
- Active Record のマルチパラメータ属性(
アプリケーションへの実質的な影響
- これまで「
nilを渡すと例外が出る」ために、ワークアラウンドとしてnilを""に変換していた箇所があれば、その必要がなくなります。 ""を「値なし」として扱っていた挙動は維持されつつ、nilも同様に「値なし」として安全に扱われるようになります。blank?を使うことで" "(空白のみの文字列)なども「空」とみなされる点は、従来のempty?との違いになり得ますが、フォーム入力的には通常期待される挙動です(※既存コードでも多くの場合blank?を利用しているので、違和感は少ないはずです)。
- これまで「
後方互換性の観点
- これまで
nilによって意図的に例外を発生させていた、というような特殊な利用をしていない限り、挙動は「エラーにならなくなる」という改善として受け取れます。 - もしマルチパラメータ用の値に、
" "(スペースのみ)を意図的に「非空」として扱いたいような非常にニッチなケースがあれば、今回のblank?への変更で「空」と見なされるようになる可能性があります。
- これまで
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57271
- 対象メソッド関連:
- String#empty?(Ruby 標準)
https://ruby-doc.org/3.4.1/String.html#method-i-empty-3F - NilClass(Rails API)
https://edgeapi.rubyonrails.org/classes/NilClass.html - String#blank?(Rails 拡張)
https://edgeapi.rubyonrails.org/classes/String.html#method-i-blank-3F - NilClass#blank?(Rails 拡張)
https://edgeapi.rubyonrails.org/classes/NilClass.html#method-i-blank-3F
- String#empty?(Ruby 標準)
#50889 Introduce ActiveSupport::TestCase.around
マージ日: 2026/5/5 | 作成者: @seanpdoyle
- 概要 (1-2文で)
Rails のActiveSupport::TestCaseにaroundコールバック(フック)が追加され、各テストのsetup〜teardownを1つの「囲い込み」処理として扱えるようになりました。これにより、RSpec などで馴染みのある「around フック」を Rails 標準のテストでもシンプルに使えるようになります。
- 変更内容の詳細
2-1. ActiveSupport::TestCase.around が追加
ActiveSupport::TestCase に around コールバックが導入され、以下の順序で実行される処理全体を 1 つのフックで囲めます。
setup- 実際のテストメソッド (
test "..." do ... end) teardown
PR の説明にある通り、ブロックには テストクラスのインスタンス (test_case) と実行ブロック (&block) が渡されます。
class MyTest < ActiveSupport::TestCase
around do |test_case, &block|
# 例: あるクライアントをスタブした状態でテストを実行
Client.with(stubbed: true, &block)
end
def setup
# 通常の setup
end
test "something" do
# この中身が Client.with(stubbed: true) の中で実行される
end
def teardown
# 通常の teardown
end
end上記の around ブロックは、内部的には
around do |test_case, &block|
# setup → test → teardown を表す「ひとかたまりの block」が渡される
Client.with(stubbed: true) do
block.call
end
endというイメージで動作します。
2-2. 実装位置
実装は activesupport/lib/active_support/testing/setup_and_teardown.rb に追加されています。ActiveSupport::TestCase が持つ既存の setup / teardown に続く、テスト用コールバック機構の一部として around が組み込まれました。
Rails には既に Controller / Job / Mailer などで利用されている Active Support のコールバックシステムがあるため、それと同じ仕組み・記法で around を利用できます。
2-3. テストとファイル構成の変更
- 新規テスト:
activesupport/test/testing/around_callback_test.rbaroundコールバックの挙動(順序・ネスト・複数定義など)がテストされています。 - 既存の teardown 周りのテストファイル名を整理するため、
after_teardown_test.rbがcallbacks_test.rbにリネームされています(PR 説明にあるが、この diff ではリネームというより around 用テスト追加がメイン)。
2-4. 典型的な利用例
- 一時的なコンテキストや接続の差し替え:
class ApiClientTest < ActiveSupport::TestCase
around do |test_case, &block|
ApiClient.with_base_url("https://stubbed.example.com") do
block.call
end
end
end- グローバル設定の一時変更:
class FeatureFlagTest < ActiveSupport::TestCase
around do |test_case, &block|
old_value = Feature.enabled?(:new_ui)
Feature.enable!(:new_ui)
block.call
ensure
Feature.set!(:new_ui, old_value)
end
end- トランザクション・外部リソースのラップ(※DB は Rails が標準でトランザクションフィクスチャを持つので、別リソース向けなどに向いている):
class ExternalServiceTest < ActiveSupport::TestCase
around do |test_case, &block|
ExternalService.transaction do
block.call
raise ActiveRecord::Rollback
end
end
end- 影響範囲・注意点
- 対象:
ActiveSupport::TestCaseを継承する Rails 標準のテスト(モデルテスト、コントローラテスト、システムテストなど)。
ただし、Minitest::Testを直接継承しているテストには影響しません。 - 既存の
setup/teardownとの関係:aroundは「setup + test本体 + teardownの外側」を囲むイメージです。- したがって、
around内でblock.callを実行しないと、setup/ テスト本体 /teardownが一切実行されません。
- 複数の around の組み合わせ:
- 複数の
aroundを定義した場合、コールバック順序(LIFO / FIFO)やネストのされ方は Active Support の一般的なコールバックと同様です。 - 内外の順番を意識しないと、想定と違う順序で環境のセットアップ・クリーンアップが行われる可能性があります。
- 複数の
- minitest-hooks との競合:
既にminitest-hooksを使ってaroundを導入している場合、ActiveSupport::TestCase側のaroundと競合する可能性があります。
Rails 標準のaroundに移行する場合は、二重適用・メソッド名衝突などに注意してください。 - 実行コスト・複雑さ:
aroundはテスト全体をラップできるため便利な一方で、ロジックが重いものを入れるとテスト全体のパフォーマンス悪化や理解コスト上昇につながります。
簡潔なスコープ制御・設定/復元処理に留めるのが望ましいです。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/50889
- Minitest が
aroundをサポートしない経緯: - 既存の around フック利用例に近いもの(別ライブラリ):
- minitest-hooks: https://github.com/jeremyevans/minitest-hooks
- Active Support コールバックの一般的な仕組み:
- Rails Guides: Active Support Callbacks(英語版)
https://guides.rubyonrails.org/active_support_callbacks.html
- Rails Guides: Active Support Callbacks(英語版)
#50320 Tag Builders: render keywords as dasherized HTML attributes
マージ日: 2026/5/5 | 作成者: @seanpdoyle
- 概要 (1-2文で)
Rails のtagヘルパー等で、属性にネストした Hash を渡したときに、キーを「ダッシュ区切り(dasherize)した HTML 属性名」として展開して出力できるようになりました。これにより、Stimulus/Turbo のdata-*だけでなく、Alpine.js のx-*や htmx のhx-*形式の属性も、Ruby 側で自然なハッシュ記法のまま生成しやすくなっています。
- 変更内容の詳細(サンプルコードを含めて)
2-1. ネストした Hash を HTML 属性に展開する挙動の追加
これまでの Rails では、data: については data: { controller: "..." } → data-controller="..." のような展開がありましたが、hx: や x: など任意のプレフィックスに対して、かつ「任意にネストした Hash」を属性に落とし込むような標準サポートはありませんでした。
この PR により、tag ビルダー (例: tag.div, tag.button) で、次のようなコードが書けます:
tag.button "POST to /clicked",
hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }出力は以下のようになります:
<button
hx-post="/clicked"
hx-swap="outerHTML"
hx-data="{"json":true}"
>
POST to /clicked
</button>ポイント:
- トップレベルの
hx:キーが「プレフィックス」として扱われる - その中の Hash のキーは dasherize されて、
hx-post,hx-swap,hx-dataという属性名になるpost→hx-postswap→hx-swapdata→hx-data
- 値がさらにネストした Hash (
data: { json: true }) の場合、適切に JSON へシリアライズされ、属性値としてエスケープされて格納される
→hx-data="{"json":true}"
Stimulus/Turbo でおなじみの:
tag.div data: { controller: "users", action: "click->users#save" }
# => <div data-controller="users" data-action="click->users#save"></div>というスタイルが、hx: や x: にも自然に拡張できるイメージです。
2-2. 実装箇所
actionview/lib/action_view/helpers/tag_helper.rb- Tag ビルダーで、属性ハッシュを処理するロジックに、任意のネスト Hash を「ダッシュ区切り属性名」に展開する処理が追加。
- 具体的には、
hx: { foo_bar: ... }のような場合にhx-foo-bar="..."という属性に変換するような仕組み。
2-3. テストの追加
以下にテストが追加されています。
actionview/test/template/tag_helper_test.rbtagヘルパー単体で、ネスト Hash → 属性展開の挙動を確認するテストが追加。
actionview/test/template/form_helper_test.rbactionview/test/template/form_helper/form_with_test.rbform_tagやform_with経由でも同様の属性構築が期待通り動くことを確認するテストが追加。
2-4. CHANGELOG の更新
actionview/CHANGELOG.md- Action View に「タグビルダーがネストした Hash から dasherized な HTML 属性を生成できるようになった」という新機能として記載。
- 影響範囲・注意点
対象範囲
ActionView::Helpers::TagHelperを利用しているすべてのコードtag.div,tag.button,form_with,form_for,form_tag内で生成される各種タグの属性処理に影響します。
後方互換性
- 既存の
data: { ... }の挙動はそのまま維持されつつ、より一般的なプレフィックス (hx:,x:等) にも同様の変換ロジックが適用される形です。 - 通常の単層 Hash (
class: "btn",hx_post: "/..."など) は従来通り動作します。
ネストさせたときにのみ、新しい展開ロジックが効きます。
- 既存の
注意点・設計上のポイント
- 「トップレベルのキー」(例:
hx:) と「その値の Hash のキー」(例:post,swap,data) を組み合わせて属性名を構築する前提になっています。 - ネストがさらに深くなる場合 (
hx: { data: { json: { foo: :bar } } }) については、基本的に JSON シリアライズされるため、「プレフィックス + さらにダッシュでつなぐ」形ではなく「1 属性の JSON 値」として表現されます。この点はdata: { foo: { bar: :baz } }と同じ考え方に沿うと考えられます。 - Hash のキーは dasherize されるため、
foo_bar→foo-barになります。JS 側の属性名と揃えるために、キー名・キャメルケース/スネークケースの扱いには注意してください。
- 「トップレベルのキー」(例:
- 参考情報 (あれば)
PR 本文で挙げられている対象フレームワーク
- Stimulus: https://stimulus.hotwired.dev
- Turbo: https://turbo.hotwired.dev
- Alpine.js: https://alpinejs.dev
- htmx: https://htmx.org
利用イメージ (Alpine.js など)
tag.div x: { data: { foo: :bar }, on: { click: "doSomething()" } }
# => <div x-data="{"foo":"bar"}" x-on-click="doSomething()"></div>このように、サーバーサイドで Ruby の Hash を自然に書きつつ、各種フロントエンドフレームワーク用の属性を柔軟に生成できるようになった、というのが本 PR の主旨です。
#57240 Move generated kamal gem into development group
マージ日: 2026/5/5 | 作成者: @jaykava
- 概要 (1-2文で)
Rails のアプリケーション生成時に自動で追加されるkamalgem を、トップレベルではなくgroup :development内に移動する変更です。これにより、Kamal が本番環境の Bundle からデフォルトで外れ、開発時専用のデプロイツールとして扱われます。
- 変更内容の詳細
背景・動機
- 現状の
rails newでは、生成されるGemfileに以下のような行がトップレベルに入ります:rubygem "kamal", require: false - その結果、「デプロイ用ツールである Kamal」が、本番環境用の
bundle install --without development testなどをしていても、トップレベル依存として本番バンドルに含まれてしまう問題がありました。 - この PR は「Kamal は開発時のデプロイツールであり、本番コードの実行に不要」という前提に沿って、Gemfile 上の位置づけを見直しています。
Gemfile テンプレートの変更
変更対象: railties/lib/rails/generators/rails/app/templates/Gemfile.tt
- これまで:ruby
# 例: トップレベルに kamal gem "kamal", require: false - 変更後:
group :development内に移動rubygroup :development do gem "kamal", require: false end
実際には他の development 向け gem(web-console, listen など)と同じグループに Kamal が含まれる形になります。
これにより、RAILS_ENV=production bundle install などで development グループを除外した際、Kamal はインストール対象から外れます。
ジェネレータの挙動とテスト
変更対象テストファイル:
railties/test/generators/app_generator_test.rbrailties/test/generators/api_app_generator_test.rb
テストで担保しているポイント:
- 通常のアプリ生成 (
rails new my_app)- 生成された
Gemfileにおいて、Kamal がgroup :development内に含まれていることを確認。
- 生成された
- API モードのアプリ生成 (
rails new my_app --api)- API アプリでも同様に Kamal が development グループに入っていることを確認。
- 既存の API 用 Gemfile 形状(特に assets 周りを含む Kamal の設定)との整合性をテスト。
--skip-dev-gemsオプションrails new my_app --skip-dev-gemsでは、development グループ用の gem が Gemfile に含まれないため、Kamal も Gemfile に現れないことを確認。
--skip-kamalオプション- 既存オプションの挙動維持:
rails new my_app --skip-kamalを指定した場合は、Kamal 自体が Gemfile に出現しないことを確認。
- 既存オプションの挙動維持:
開発者向けに想定される生成結果イメージ:
# Gemfile (抜粋)
# 通常の gem 群
gem "rails", "~> 7.2.0"
gem "pg"
# ...
group :development do
gem "kamal", require: false
# その他 development 用 gem
end- 影響範囲・注意点
影響範囲
- 新しく
rails newで生成するアプリケーションのみが対象です。 - 既存アプリの Gemfile は自動で書き換えられませんが、同じ問題(Kamal がトップレベルにある)が気になる場合、自分で
group :developmentに移動しても良いです。 - 通常/ API モードいずれのアプリ生成でも、Kamal は「開発グループの gem」として扱われます。
注意点
- 本番環境で Kamal を実行したい場合
- サーバ上でデプロイ作業を行う際に
bundle exec kamal ...を使う運用を想定している場合、production 環境でbundle install --without developmentのようにしていると Kamal がインストールされません。 - その場合:
- デプロイ用の環境(ローカルCIサーバなど)だけは development グループも含めて
bundle installする - もしくは「アプリ実行環境」と「Kamal 実行環境」を分ける(ローカル or CI で Kamal からリモートにデプロイする) などの運用設計が必要です。
- デプロイ用の環境(ローカルCIサーバなど)だけは development グループも含めて
- サーバ上でデプロイ作業を行う際に
--skip-dev-gems/--skip-kamalの組み合わせ--skip-dev-gemsを付けると Kamal を含む全ての development グループ gem が除外されます。--skip-kamalは Kamal 単体の除外用であり、development グループの他の gem には影響しません。- これらの挙動は引き続き既存通りであることがテストで担保されています。
- 参考情報 (あれば)
- 対応 issue: #57205 — 「Kamal が本番 bundle に含まれてしまう」問題への対応 PR。
- 実行されたテストコマンド:
- 一般アプリのジェネレータテスト:bash
bundle exec ruby -Irailties/test \ railties/test/generators/app_generator_test.rb \ -n "/kamal|skip_dev_gems/" - API アプリのジェネレータテスト:bash
bundle exec ruby -Irailties/test \ railties/test/generators/api_app_generator_test.rb \ -n "/test_api_modified_files|test_kamal_deploy_yml_excludes_asset_path_for_api_apps/"
- 一般アプリのジェネレータテスト:
- 変更ファイル:
- Gemfile テンプレート:
railties/lib/rails/generators/rails/app/templates/Gemfile.tt - ジェネレータテスト:
railties/test/generators/app_generator_test.rbrailties/test/generators/api_app_generator_test.rb
- Gemfile テンプレート:
#57301 Document _came_from_user? and _for_database attribute method suffixes [ci skip]
マージ日: 2026/5/5 | 作成者: @joaomarcos96
- 概要 (1-2文で)
Rails のActiveRecord::AttributeMethods::BeforeTypeCastが内部的に提供している*_for_databaseと*_came_from_user?という属性メソッドサフィックスについて、公式ドキュメントに追記した PR です。挙動の変更は一切なく、ドキュメントとコメントの整備のみです。
- 変更内容の詳細(あればサンプルコードも含めて)
対象となるモジュール
ActiveRecord::AttributeMethods::BeforeTypeCast では、以下のように3つのサフィックス付き属性メソッドを定義しています:
attribute_method_suffix "_before_type_cast", "_for_database", parameters: false
attribute_method_suffix "_came_from_user?", parameters: false従来は _before_type_cast だけが rdoc に説明されており、_for_database / _came_from_user? は、コードを読まない限りほぼ知られない状態でした。
追加されたドキュメントの要点
この PR では、モジュールレベルの rdoc に下記 2 つのサフィックスの説明と、Book モデル+enum を用いた簡単な例が追加されています。
*_for_database
- 対象:
book.status_for_databaseのように、属性名 +_for_databaseというメソッド - 役割:
「その属性が実際に DB に保存される際の値」(=type キャスト・enum 変換などを適用した後の値)を取得するためのディスパッチ先 - 元々存在していた API:
read_attribute_for_database(:status)attributes_for_databaseからも利用できる機能だが、*_for_databaseというメソッド形式はドキュメント化されていなかった、という位置づけです。
イメージ例(enum を利用した Book モデル):
class Book < ApplicationRecord
enum status: { draft: 0, published: 1 }
end
book = Book.new(status: :published)
book.status # => "published" (enum のキー)
book.status_before_type_cast # => "published" (元入力)
book.status_for_database # => 1 (DB に保存される実際の整数値)*_came_from_user?
- 対象:
book.status_came_from_user?のように、属性名 +_came_from_user?というメソッド - 役割:
「その属性の現在の値が、ユーザー入力(属性代入)由来かどうか」を判定するためのディスパッチ先 - これも内部的には以前から存在していましたが、コメントや rdoc に説明がなかったため、今回説明を追加しています。
イメージ例(フォーム入力などを想定):
book = Book.new
book.status_came_from_user? # => false (まだ何も代入されていない)
book.status = 'published'
book.status_came_from_user? # => true (ユーザー入力によってセットされた値)※ 実際の判定ロジックは ActiveRecord の内部実装に依存しますが、概念としては「DB 読み込みやデフォルト値ではなく、アプリ側からの代入でセットされた値か」を知るためのフラグです。
コメントの追加
内部メソッドに、他の _before_type_cast と同様のコメントが追加されました。
attribute_for_databaseに:
# Dispatch target for *_for_database attribute methods.attribute_came_from_user?に:
# Dispatch target for *_came_from_user? attribute methods.これにより、*_for_database / *_came_from_user? がどのメソッドにディスパッチされるのか、コードリーディング時にも分かりやすくなっています。
- 影響範囲・注意点
挙動変更なし
既存の動作・API に一切変更はありません。
・*_for_database/*_came_from_user?メソッドは以前から利用可能であり、今回の PR はそれを公式に文書化しただけです。ドキュメント依存のユーザーにとっての影響
- これまで「知られざる内部 API」っぽかった
status_for_database/status_came_from_user?などが、公式にサポートされている振る舞いとして分かりやすくなります。 - enum・独自 type・暗号化 attribute などを扱うときに、
- 「DB に実際に保存される値」を直接確認したい
- 「これはユーザー入力起点の値かどうか」を判別したい
といった場面で、これらのメソッドを安心して利用しやすくなります。
- これまで「知られざる内部 API」っぽかった
将来の互換性の観点
- ドキュメントに明示されたことで、「半ば内部実装」に見えていたこれらの方法が、よりパブリックな契約に近づいたと解釈できます。
- gem やアプリケーションがこれらに依存しても、将来的に破壊的変更になりにくくなる、という意味でプラスです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57301
- 関連する API:
ActiveRecord::AttributeMethods::BeforeTypeCastModel#attribute_before_type_castModel#read_attribute_for_database(name)Model#attributes_for_database
#57129 Use Hash#each_key instead of Hash#keys.each
マージ日: 2026/5/5 | 作成者: @ashwin47
- 概要 (1-2文で)
Rails内部で、Hash#keys.eachを使っていた箇所の一部をHash#each_keyに置き換え、余計な配列生成を避けることでパフォーマンスとメモリ効率をわずかに改善するリファクタリングです。ハッシュを反復中に変更しない箇所のみを安全に置き換えており、挙動の変更はほぼありません。
- 変更内容の詳細
背景・意図
Hash#keys.eachは以下のようなコードです:rubyhash.keys.each do |key| # ... endこれは
hash.keysで「全キーを配列にコピー」- その配列に対して
eachでループ
という2段階処理になり、中間配列の生成コスト(メモリ・GC)が発生します。
一方、
Hash#each_keyは:rubyhash.each_key do |key| # ... endのように、ハッシュ内部構造を直接たどってキーを反復するため、中間配列を生成せずに済みます。
PR #17099 でも同種のクリーンアップが行われており、その続きとして今回のPRが入っています。
実際の変更点
PR本文では以下2ファイルと書かれていますが、今回の diff では3ファイルが変更対象として挙がっています(実際のコード差分は1行ずつの軽微な修正):
activemodel/lib/active_model/schematized_json.rbactiverecord/test/cases/connection_adapters/connection_handler_test.rbactivesupport/test/env_configuration_test.rb
いずれも本質的には同じパターンの置き換えです:
変更前(イメージ):
hash.keys.each do |key|
# ハッシュはこのループ中に変更されない
end変更後:
hash.each_key do |key|
# ハッシュはこのループ中に変更されない
end重要なのは、「ループ中に delete などでハッシュを破壊的に変更しない」箇所だけが置き換え対象になっている点です。
置き換え対象に していない 箇所
PR説明中では、以下の3箇所はそのまま keys.each が残されていると明記されています:
hash/keys.rbstrong_parameters.rbrouting/mapper.rb
これらではループ内で delete などによりハッシュを変更しているため、先に keys でスナップショット(配列コピー)を取ってから反復する必要があるケースです。このような場合に each_key に変えると、繰り返し中に対象ハッシュが変化してバグや予期せぬ動作を招く可能性があるため、意図的に残されています。
- 影響範囲・注意点
影響範囲は内部実装のパフォーマンスに限られ、外部APIの仕様変更や互換性破壊はありません。
変更対象となった箇所ではループ中にハッシュを変更しておらず、挙動は
keys.eachと論理的に等価です。メモリ使用量がごくわずかに減り、GC負荷もわずかに軽くなる可能性がありますが、多くのアプリケーションでは体感できない程度の微小な改善です。
自分のアプリ・ライブラリ側でも、以下のようなループは
each_key/each_value/each_pairに置き換え可能か検討できます:ruby# 変更前 hash.keys.each do |key| # ループ中に hash を変更しない end # 推奨パターン hash.each_key do |key| # ... endただし、ループ中に
hash.delete(key)やhash[some_key] = ...等で構造を変える場合は、従来どおりhash.keys.eachでスナップショットを取る方が安全です。
- 参考情報 (あれば)
- Rubyのドキュメント:
- 類似のクリーンアップPR(本PRで参照されているもの):
- Rails PR #17099:
Use each_key and each_value instead of keys.each and values.each(ハッシュ反復周りの最適化)
- Rails PR #17099:
#55826 Add conflicting record ID to validates_uniqueness_of error details
マージ日: 2026/5/5 | 作成者: @bvicenzo
- 概要 (1-2文で)
validates_uniqueness_of/validates :field, uniqueness: trueの検証に失敗した場合、errors.detailsに「衝突している既存レコードの主キーID(existing_id)」が含まれるようになりました。これにより、どのレコードと一意制制約がぶつかったかをアプリ側で特定しやすくなります。
- 変更内容の詳細
2-1. 何が変わったか
ActiveRecord の ActiveRecord::Validations::UniquenessValidator の重複チェック実装が以下のように変更されています。
以前:
検証対象と同じ値を持つレコードが存在するかを.exists?で確認していた
→ 存在するかどうか (true/false) しか分からない変更後:
.pick(klass.primary_key)を使って、衝突している既存レコードの主キー値を取得する
→ 存在する場合は、その ID をerrors.detailsに:existing_idとして格納
具体的には、バリデーションが失敗したときのエラー詳細が次のように変わります。
変更前
class Topic < ApplicationRecord
validates :title, uniqueness: true
end
topic = Topic.new(title: "Existing Topic")
topic.valid? # => false
topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic" }]変更後
topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic", existing_id: 1 }]existing_id には、衝突しているレコードの primary_key(通常は id)が入ります。
スコープ付き一意性 (validates :title, uniqueness: { scope: :user_id } など) に対しても同様に適用されます。
2-2. 実装上のポイント
- DB問い合わせ部分で
.exists?→.pick(klass.primary_key)に変更。- どちらも
LIMIT 1で 1 行だけ取得するクエリになるため、パフォーマンス特性はほぼ同等。 .pickは該当行があれば主キーIDを返し、なければnilを返す。
- どちらも
- 衝突レコードが見つかった場合のみ、
errors.detailsの要素ハッシュにexisting_id: <主キー値>を追加。 - I18n 関連・ユニークネス関連のテストがこの新しい振る舞いに合わせて更新されています。
activerecord/CHANGELOG.mdに仕様変更として追記済み(公式にサポートされる振る舞い)。
2-3. 利用イメージ
JSON API で 409/422 エラーに「どのレコードと衝突したか」を載せたいケースなどで、そのまま使いやすくなります。
# 例: API レスポンス整形
record.valid?
if record.errors.added?(:email, :taken)
existing_id = record.errors.details[:email].first[:existing_id]
render json: {
error: "email_taken",
message: "Email is already taken by another user",
conflict_user_id: existing_id,
conflict_user_url: user_url(existing_id)
}, status: :unprocessable_entity
end- 影響範囲・注意点
errors.detailsのスキーマが変わる
一意性バリデーションエラーの details 要素に:existing_idキーが増えます。
既存コードでerrors.details[:attr].firstをそのままシリアライズしている場合、追加フィールドがクライアントに見えるようになります。- もし既存クライアントが「キーを厳密にチェックしている」場合は、API 仕様として許容するか、サーバ側でフィルタリングが必要な可能性があります。
セキュリティ / 情報漏えい面の検討
existing_idによって、既存レコードの ID 情報がクライアントに渡せるようになります。- 自分以外のリソースIDをユーザーに見せたくないケース(マルチテナントでID自体を秘匿したい、など)の場合は、レスポンス整形で
existing_idを露出しないようにする必要があります。 - 逆に「ID をキーにしたリンクを返したい」など、積極的に ID を利用したい API では便利になります。
パフォーマンス
ベンチマーク(SQLite, Ruby 3.4.6, AR 8.1.0.beta1)では:.exists?: 約 9.6k i/s.pick(:id): 約 9.7k i/s
と誤差範囲内の差で、実質的なパフォーマンス劣化はほぼ無しとみなせます。
主キーがカスタムな場合
klass.primary_keyを使用しているため、id以外を主キーにしているモデルでも、その主キー値がexisting_idとして格納されます。existing_primary_keyではなくexisting_idというキー名である点は注意(意味的には「existing recordの主キー」)。
- 参考情報 (あれば)
- Rails Discuss の提案スレッド
https://discuss.rubyonrails.org/t/proposal-include-existing-record-id-in-uniqueness-validation-error-details/89810 - 変更ファイル(主に見るべきもの)
activerecord/lib/active_record/validations/uniqueness.rbactiverecord/test/cases/validations/uniqueness_validation_test.rb
- 実際に取りたい情報は
record.errors.details[:attr].map { |d| d[:existing_id] }.compactのようにして複数エラーに対応可能です。
#57247 Use ** in .dockerignore template for recursive directory exclusions
マージ日: 2026/5/5 | 作成者: @bogdan
- 概要 (1-2文で)
Rails がrails new --dockerなどで生成する.dockerignoreテンプレートを修正し、/log/*や/tmp/*といった「1階層だけマッチするパターン」を/log/**のような「再帰的にすべてのサブディレクトリを含めてマッチするパターン」に変更した PR です。これにより、ログ・tmp・storage などの配下にある深い階層のファイルも Docker build context から確実に除外されるようになります。
- 変更内容の詳細
具体的な変更
.dockerignore テンプレート (railties/lib/rails/generators/rails/app/templates/dockerignore.tt) が以下のように変更されています。
パターンの変更
単一階層マッチ * → 再帰マッチ ** に変更:
- /log/*
- /tmp/*
- /storage/*
- /app/assets/builds/*
+ /log/**
+ /tmp/**
+ /storage/**
+ /app/assets/builds/**Docker の .dockerignore では:
dir/*…dir直下のファイル/ディレクトリのみマッチdir/**…dir配下のあらゆる階層のファイル/ディレクトリを再帰的にマッチ
となるため、** にすることで、深い階層のファイルも確実に build context から除外されます。
冗長なエントリの削除
もともと .dockerignore には、/tmp 配下を個別に除外する行がありましたが、/tmp/** を導入したことで冗長になったため削除されています。
- /tmp/pids/**
- /tmp/storage/**/tmp/** が /tmp/pids や /tmp/storage の中身もすべてカバーするためです。
.keep の例外指定を整理
tmp 以下に置かれた .keep ファイルを context に含めるための否定パターン(除外ルールの例外)をまとめ直しています:
/tmp/**
!/tmp/.keep
!/tmp/pids/.keep
!/tmp/storage/.keep/tmp/**ですべてを除外- その後
!プレフィックスで.keepファイルだけを再度含める(.dockerignore の挙動: 後勝ち)
という形で、tmp 配下のディレクトリ構造を保ちつつ、中身の実データは送らないようにしています。
- 影響範囲・注意点
どこに影響するか
- Rails が生成する
.dockerignoreが変わるため、今後rails newで Docker 対応のアプリを作成したときのデフォルト挙動が変わります。 - 既存プロジェクトには自動で適用されないので、恩恵を受けたい場合は自分で
.dockerignoreを更新する必要があります。
具体的に改善される点
旧パターンのままだと除外されていなかった代表例:
tmp/cache/assets/sprockets/v4.0.0/ab/abcdef1234...
→ Sprockets のアセットキャッシュが深くネストされており、/tmp/*では除外しきれなかったが、/tmp/**により除外される。tmp/pids/server.pid
→ サーバ PID ファイルもtmp/pids/以下のため、旧パターンでは build context に含まれうる。storage/ab/cd/abcdefghijklmn
→ Active Storage の Disk サービスはキーをプレフィックスで2階層に分けて保存するため、ユーザのアップロードファイルが Docker イメージの build context に含まれてしまう可能性があった。
これらがすべて .dockerignore により除外されるようになります。
結果として:
- Docker build context が小さくなり、ビルド時間・アップロード時間(リモートビルドの場合)が短縮される。
- 誤ってユーザーのアップロードファイルやキャッシュ・PID などを Docker イメージに含めてしまうリスクが下がる。
注意点
.dockerignoreの挙動は.gitignoreと完全に同じではありません。特に**の再帰マッチは Docker 固有の挙動です。
カスタマイズしている場合は、Docker ドキュメントを前提にパターンを確認してください。- 既存プロジェクトで手動マージする際:
- すでに
/tmp/**などを独自に書いている場合は重複しないように統合する。 /tmp配下であえて含めたいファイル(例: 独自に何か生成しているもの)がある場合は、!/tmp/your_fileのように例外指定が必要です。
- すでに
- 参考情報 (あれば)
- Docker 公式ドキュメント:
.dockerignoreのマッチング仕様
https://docs.docker.com/build/concepts/context/#matching - この PR:
https://github.com/rails/rails/pull/57247
#57208 Add ActiveJob::Attributes to persist data between steps
マージ日: 2026/5/5 | 作成者: @bdewater-thatch
- 概要 (1-2文で)
Rails 8.1で導入された Active Job Continuation(マルチステップジョブ)に対して、ステップ間で一時的なデータを安全に保持できるActiveJob::Attributesが追加されました。Active Model Attributes をベースに、ジョブのシリアライズ/デシリアライズを意識せずに型付き属性を使って値を永続化できます。
- 変更内容の詳細
ActiveJob::Attributes の追加
- 新モジュール
ActiveJob::Attributesが追加されました。 - 中身は基本的に
ActiveModel::Attributesの API をそのまま使えるようにしたものです。attributeDSL による宣言- 型付き属性 (
:string,:integer,:datetime, 独自 type など) - デフォルト値、lazy default(Proc)、cast、serialize など Active Model Attributes の機能を利用可能
イメージとしては「Active Job 版 Active Model Attributes」で、ジョブ引数とは別に「ジョブの状態を持つための属性レイヤー」を提供するものです。
Continuable へのデフォルト組み込み
ActiveJob::Continuableにinclude ActiveJob::Attributesが追加されています。- つまり、Continuation を使ったマルチステップジョブでは、特別な設定なしに
attributeが使えます。
class EnrollmentJob < ApplicationJob
include ActiveJob::Continuable
# ステップ間で保持したいデータ
attribute :payment_token, :string
attribute :user_metadata, :json
step :tokenize_payment
step :charge_payment
def tokenize_payment(step)
self.payment_token = PaymentGateway.tokenize(enrollment.user.payment_instrument)
# この時点で payment_token はジョブのペイロードに埋め込まれる
step.advance!
end
def charge_payment(step)
# 再キューイング後でも payment_token が復元されている
PaymentGateway.charge(token: payment_token)
step.advance!
end
end上記のように、step の間で payment_token をインスタンス変数や外部テーブルに自前保存することなく、安全に保持できます。
既存案(step の戻り値を自動保存)を採用しなかった理由
PR本文で触れられているが、以下のような API は採用されていません:
payment_token = step(:tokenize_payment_instrument, store: true) do
PaymentGateway.tokenize(enrollment.user.payment_instrument)
endこの案を見送った理由:
attributeベースの方がシンプルかつ明示的である- Continuable がカーソルを
step.cursor/set!/advance!で明示的に扱っている現状の API と整合的
(フレームワークが「勝手に戻り値を保存する」より、「属性として明示的に状態を持つ」方針に揃えている)
Active Model 側の微調整
activemodel/lib/active_model/attributes.rbにも 1 行変更があり、ActiveJob::Attributesからの利用に必要な小さな調整が入っています(主に include まわりの都合付けで、既存利用への影響は極小と考えられます)。
- 影響範囲・注意点
対象範囲
- 新機能なので既存ジョブはそのまま動作します。
ActiveJob::Continuableを使うジョブでは、デフォルトでActiveJob::Attributesが使えるようになります。- Continuable を使っていない通常の Active Job でも、
include ActiveJob::Attributesすれば利用可能です。
使いどころ
- マルチステップ処理の「中間状態」を一時的に保持したいが、DB のカラム追加や専用テーブル作成まではしたくないケース。
- 例:
- 外部 API のトークン、セッション情報
- ステップ 1 で計算した一時的な集計値
- 後続ステップでのみ必要な、生存期間の短いデータ
ジョブ引数との違い
- ジョブ引数は「ジョブ作成時に渡すもの」であり、通常は immutable な前提で扱われます。
ActiveJob::Attributesは「ジョブのライフサイクルの中で変化する“状態”」を持つための仕組みです。- ジョブを再キューイング・再開しても、これらの属性はシリアライズ/デシリアライズされて維持されます。
シリアライズサイズへの配慮
- 属性はジョブのペイロードに載るため、扱うデータはできるだけコンパクトなものに限定するべきです。
- 大きなオブジェクト・バイナリなどを保存すると、キューシステム(Sidekiq, Resque など)のメッセージサイズ制限に引っかかる可能性があります。
- 「必要な識別子だけ属性に保存し、実データは再度取得する」ような設計が推奨されます。
型・変換の注意
- Active Model Attributes の型システムに依存するため、
- カスタム type を使うなら、Active Model 側で type を定義してから利用する
- JSON や Array、Hash などもサポートされるが、シリアライズ表現(JSON / String 化)に注意する
といった点は既存の Active Model Attributes と同様です。
- Active Model Attributes の型システムに依存するため、
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57208
- 背景ディスカッション: https://discuss.rubyonrails.org/t/proposal-activejob-attributes-to-persist-data-between-steps/91051
- Active Job Continuation: https://api.rubyonrails.org/classes/ActiveJob/Continuation.html
- Active Model Attributes: https://api.rubyonrails.org/classes/ActiveModel/Attributes.html
この PR により、Active Job のマルチステップ処理で「一時的だが、再開時にも必要な状態」を持つための公式なパターンが用意され、従来の serialize/deserialize のオーバーライドや独自ストレージ実装が不要になることが期待されます。
#56450 Fix ActionMailer::Base.mail dispatch in 8.1+
マージ日: 2026/5/5 | 作成者: @afurm
- 概要 (1-2文で)
Rails 8.1 でActionMailer::Base.mailをクラスメソッドとして直接呼び出した際にActionNotFoundが発生するリグレッションを修正し、8.0 と同じ挙動(クラスレベル配送)を復元する PR です。mailが ActionMailer の「アクション」として常に認識されるようにし、その挙動を担保する回帰テストが追加されています。
- 変更内容の詳細 (サンプルコード含む)
問題の背景
Rails 8.1 以降で、以下のようなコードが動かなくなっていました:
ActionMailer::Base.mail(
to: "user@example.com",
from: "no-reply@example.com",
subject: "Hello",
body: "Hi"
).deliver_now従来(〜Rails 8.0)はこれが許可されていましたが、8.1 では mail が「有効なメールアクション(action_methods)」として扱われなくなり、Action Mailer の内部ディスパッチで ActionNotFound が発生するようになっていました。
修正内容
1) ActionMailer::Base の action_methods に常に mail を含める
actionmailer/lib/action_mailer/base.rb に変更が入り、ベースクラス (ActionMailer::Base) を直接使う場合、mail が常にアクションとしてみなされるようになりました。
イメージとしては、Action Mailer が action_methods を決定するロジックに「ActionMailer::Base に関しては mail を特別扱いして常に含める」という処理が入った形です。
その結果、ActionMailer::Base.mail 呼び出し時も内部的には通常の Action Mailer のアクションディスパッチフローに乗り、ActionNotFound が起きなくなります。
2) 回帰テストの追加
actionmailer/test/base_test.rb に、以下のようなテストが追加されています:
ActionMailer::Base.mailを直接呼び出してdeliver_now/deliver_laterできること- その呼び出しが Action Mailer のアクションディスパッチを通って成功すること(=
ActionNotFoundにならないこと)
テストは PR 説明にある通り、以下で実行確認されています。
bundle exec ruby -Iactionmailer/test actionmailer/test/base_test.rb -n "/ActionMailer::Base.mail/"利用イメージ
この PR 適用後は、Rails 8.1+ でも以下のような「簡易ワンショットメール送信」が再び安全に使えます。
# メイラークラスを定義しなくても、ベースクラスだけで送信可能
ActionMailer::Base.mail(
to: "user@example.com",
from: "no-reply@example.com",
subject: "Quick mail",
body: "This is a quick one-off email."
).deliver_now- 影響範囲・注意点
影響範囲
ActionMailer::Base.mail(...)をクラスメソッドとして直接呼んでいる既存コードに対して、Rails 8.1 で発生していたActionNotFoundが解消され、Rails 8.0 と同じ挙動になります。- 独自のメイラークラス(
UserMailer < ApplicationMailerなど)で、通常のアクション(def welcome_email ... end)を使っているケースには基本的に影響はありません。
互換性
- 明確に「8.0 の挙動を復元する」ことが目的のため、既存の 8.0 向けコードとの後方互換性を回復する変更です。
ActionMailer::Baseを特殊な形で継承し、action_methodsをカスタマイズしているような高度なメタプログラミングをしていない限り、副作用の可能性は低いと考えられます。
注意点
- PR 時点では CHANGELOG は更新されていないため、この挙動変更は一見分かりづらい可能性があります。Rails を 8.1 系に上げた際に
ActionMailer::Base.mail周辺が壊れた場合、この修正が入っているバージョンかを確認するとよいです。 ActionMailer::Baseを直接使ったメール送信はあくまで「簡易用途」向けであり、大量のメール送信や複雑なロジックには従来通り独自のメイラークラスの定義を推奨します。
- PR 時点では CHANGELOG は更新されていないため、この挙動変更は一見分かりづらい可能性があります。Rails を 8.1 系に上げた際に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/56450
- 対応 Issue: https://github.com/rails/rails/issues/56449
- 対象クラス:
ActionMailer::Base - テスト実行コマンド(当該変更部分のみ):bash
bundle exec ruby -Iactionmailer/test actionmailer/test/base_test.rb -n "/ActionMailer::Base.mail/"
#57288 Define as_json on ActiveStorage::Attached::One and Attached::Many
マージ日: 2026/5/5 | 作成者: @onyxblade
- 概要 (1-2文で)
has_one_attached/has_many_attachedの名前が実カラム(あるいは ignored_columns に入ったカラム)と衝突しているときに、record.to_jsonでSystemStackError(無限再帰)が発生するバグを修正するPRです。ActiveStorage::Attached::One/Manyに明示的なas_jsonを定義し、レコードの JSON シリアライズ時に安全に変換されるようにしました。
- 変更内容の詳細
問題の背景
以下のようなモデルがあるとします:
class Product < ApplicationRecord
self.ignored_columns = [:photo]
has_one_attached :photo
end
Product.select('*').first.to_json
# => SystemStackError: stack level too deepポイント:
- DB 上には
photoカラムが存在する。 - Rails 側では
self.ignored_columns = [:photo]で無視しつつ、同名でhas_one_attached :photoを定義。 Product.select('*')により、無視されるはずのphotoカラムも結果セットには含まれる。- その状態で
record.to_jsonを呼ぶと、ActiveModel の JSON シリアライズ処理の途中で、Attached::One/Attached::ManyのインスタンスがObject#as_jsonにフォールバック。 Object#as_jsonはinstance_values(インスタンス変数をすべてハッシュ化)を返すため、その中に含まれる@recordからまたas_jsonが呼ばれ…というサイクルが発生し、スタックオーバーフローになる。
スタックトレース上では、Attached::One / Many が Object#as_json を通ってしまうことが原因で、record へのバックリファレンスがシリアライズ対象になり無限再帰が発生していました。
対応内容
ActiveStorage::Attached::One と ActiveStorage::Attached::Many に as_json を定義し、Object#as_json に落ちないようにする、というローカルな修正です。
Attached::One#as_json
- 振る舞い:
- 添付済みなら、そのアタッチメント(
ActiveStorage::Attachmentレコード)のas_jsonを返す。 - 未添付なら
nilを返す。
- 添付済みなら、そのアタッチメント(
擬似コードイメージ:
# activestorage/lib/active_storage/attached/one.rb
def as_json(*)
if attached?
attachment.as_json
else
nil
end
end結果として、Product.first.to_json の中で photo が JSON 化されるときは、以下のような JSON に展開されます(概念的な例):
{
"id": 1,
"name": "Sample product",
"photo": {
"id": 10,
"blob_id": 99,
"record_id": 1,
"record_type": "Product",
"name": "photo",
"created_at": "2026-05-05T12:34:56Z"
}
}Attached::Many#as_json
- 振る舞い:
- アタッチされている全ての
ActiveStorage::Attachmentのas_jsonを配列で返す。
- アタッチされている全ての
擬似コードイメージ:
# activestorage/lib/active_storage/attached/many.rb
def as_json(*)
attachments.map(&:as_json)
endJSON は以下のような形になります(概念的な例):
{
"id": 1,
"name": "Sample product",
"photos": [
{
"id": 10,
"blob_id": 99,
"record_id": 1,
"record_type": "Product",
"name": "photos",
"created_at": "2026-05-05T12:34:56Z"
},
{
"id": 11,
"blob_id": 100,
"record_id": 1,
"record_type": "Product",
"name": "photos",
"created_at": "2026-05-05T12:35:56Z"
}
]
}なぜこれで安全なのか
ActiveStorage::Attachmentは以下のようなスカラーカラムのみを持つシンプルなモデルです:id,blob_id,name,record_id,record_type,created_at
- これらのカラムには
ActiveStorage::Attached::One/Manyのような「レコード自身へのバックリファレンス」は含まれていないため、Attachment#as_jsonを呼び出しても再帰ループは発生しません。 - そのため
Attached::*インスタンスの JSON 化は、Attachmentレコードの配列(または単一オブジェクト)に収束し、安全にシリアライズが完了します。
代替案として検討されたもの
PR説明で触れられている代替案:
ActiveRecord::Persistence#instantiate_instance_ofの中で、ignored_columnsに入っているカラムを row hash から取り除く。- これにより、
select('*')を書いていても、ignored_columnsに入っているカラムはattribute_namesに現れないようにできる。
- これにより、
- しかし、この方法は:
- ユーザーが明示的に SQL で取得したカラムが「サイレントにドロップされる」挙動変更であり、影響範囲が広い。
- ActiveStorage 以外にも「レコードをバックリファレンスする任意の Object サブクラス」には効かない。
そのため、より局所的で副作用の少ない「Attached::* プロキシに as_json を生やす」方針が選ばれています。
テスト・ドキュメント
activestorage/test/models/attached/one_test.rbactivestorage/test/models/attached/many_test.rb
にテストが追加され、今回の as_json の挙動が保証されています。
activestorage/CHANGELOG.mdにも変更が記載され、挙動変更として明示されています。
- 影響範囲・注意点
対象:
- Active Storage を利用しているアプリケーションのうち、
has_one_attached/has_many_attachedの名前が実カラム名と衝突している、- または
ignored_columnsと組み合わせてカラムを無視しつつ、同名で attachment を定義している、 - さらに
select('*')や特定のselectで無視したはずのカラムを結果に含めてしまう、 といったケースでrecord.to_jsonを呼んでいるプロジェクト。
- Active Storage を利用しているアプリケーションのうち、
この PR により、これらのケースで
SystemStackErrorが発生しなくなります。JSON 形式の変化:
- これまで
to_jsonを呼んでもエラーで落ちていたようなケースでは、今回初めて JSON が正常に返ることになります。 has_one_attachedのフィールドは「単一のAttachmentオブジェクト ornil」、has_many_attachedは「Attachmentオブジェクトの配列」として JSON に含まれるようになります。- もし
to_json/as_jsonをオーバーライドして独自形式を返したい場合は、アプリ側でas_jsonを追加実装する必要があります。
- これまで
Rails の他部分への影響:
- 修正箇所は Active Storage の
Attached::One/Manyに限定されており、ActiveRecord の汎用的なインスタンス生成や ignored_columns の扱いには手を触れていません。 - そのため、ActiveRecord 全体の挙動変更のリスクは小さいです。
- 修正箇所は Active Storage の
- 参考情報 (あれば)
- 対応する issue: #57287
「has_one_attached/has_many_attachedが属性名をシャドウするときのSystemStackError」を扱うものです。 - 対象クラス:
ActiveStorage::Attached::OneActiveStorage::Attached::ManyActiveStorage::Attachment
- 類似の問題を避けるための指針:
- カラム名と ActiveStorage の attachment 名を可能な限り衝突させない。
- どうしても同名にする必要がある場合でも、今回の修正により少なくとも
to_json時の無限再帰は解消されます。
#57162 Skip CreateUsers migration when User model already exists
マージ日: 2026/5/5 | 作成者: @johntopley
- 概要 (1-2文で)
Rails のrails generate authenticationで、すでにUserモデルが存在する場合にCreateUsersマイグレーションを自動生成しないようにし、「table already exists」エラーを避ける修正です。既存アプリに後から認証機能を追加しやすくなる変更です。
- 変更内容の詳細
2-1. 挙動の変更点
これまで:
rails generate authentication Userなどを実行すると、- 常に
db/migrate/xxxxxx_create_users.rbのようなCreateUsersマイグレーションを生成 - 既に
usersテーブルがある(=既存の User モデル+マイグレーションがある)状態でdb:migrateを実行すると、- 「table already exists」エラーで失敗
- 常に
今回の変更後:
- ジェネレータ実行前に
app/models/user.rbが存在するかをチェック - 存在する場合は
CreateUsersマイグレーションの生成をスキップ - そのため、既存の
usersテーブルがあるアプリに後から authentication generator を適用しても、テーブル作成マイグレーションが重複せずに動く
2-2. コードレベルでの変更点 (概要)
railties/lib/rails/generators/rails/authentication/authentication_generator.rb にて:
- User モデルファイルの存在チェックロジックを追加
- 例:
File.exist?(Rails.root.join("app/models/user.rb"))のような条件を導入し、真の場合はCreateUsersマイグレーションのテンプレート生成を行わないようにしている(正確なメソッド名・条件はPR本文からの推測だが、実装方針はこれ)
- 例:
- 既存のマイグレーション生成処理の前後に条件分岐を挿入し、「Userモデルがないときだけ
create_usersマイグレーションを生成」するように変更
railties/test/generators/authentication_generator_test.rb では:
- 新しい挙動に対するテストが追加
app/models/user.rbがある状態でジェネレータを実行したときにCreateUsersマイグレーションが生成されないことを検証- User モデルがない状態では従来どおりマイグレーションが生成されることも担保
railties/CHANGELOG.md:
- 上記の仕様変更が CHANGELOG に追記され、振る舞いの変更として明文化されています。
- 影響範囲・注意点
- 影響対象:
- Rails 付属の
authenticationgenerator を利用しているプロジェクト - 既存アプリに後から
rails generate authentication Userを適用するケースで特に恩恵がある
- Rails 付属の
- 注意点:
- 今回の判定は「
app/models/user.rbが存在するかどうか」で行われているため、- テーブル名は
usersだがモデルファイルが別名/別ディレクトリにある、 - あるいは
Userモデルがあってもファイル名が慣例どおりでない
といった特殊な構成をしている場合は、期待どおりに判定されない可能性があります。
- テーブル名は
- 逆に、
app/models/user.rbだけ存在していてusersテーブルがまだない場合は、マイグレーションが生成されずテーブルが作成されないので、そのような構成をとる場合は手動でマイグレーションを用意する必要があります。
- 今回の判定は「
- バックワード互換性:
- 一般的な Rails アプリ(
Userモデル=app/models/user.rb、テーブル=users)では、既存の挙動が「壊れる」ことは基本的になく、むしろ二重マイグレーションのエラーが防止される方向の変更です。
- 一般的な Rails アプリ(
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57162
- 閉じられた Issue(動機となったバグ報告): #57149
- 「既に User モデルと
usersテーブルが存在するアプリで authentication generator を使うと、db:migrateで ‘table already exists’ が発生する」という報告に対応した変更です。
- 「既に User モデルと
#57298 Fix eager_load for CPK models with explicit select
マージ日: 2026/5/5 | 作成者: @fatkodima
- 概要 (1-2文で)
複合主キー(CPK: Composite Primary Key)を持つモデルに対して、selectを明示したクエリにeager_loadを使うと正しく動かない不具合が修正されました。eager_load時の主キー列の扱いを見直し、CPK モデルでも関連を正しくプリロードできるようになっています。
- 変更内容の詳細
※ パッチ自体は非常に小さく、join_dependency.rb の条件分岐を 1 行変更し、それに対応するテストを eager_test.rb に追加したものです。実際のコード断片はリポジトリを直接参照する必要がありますが、変更の意図と動きは次のように整理できます。
背景
- ActiveRecord の
eager_loadは、関連を 1 クエリで取得するためにLEFT OUTER JOINを使います。 - その際、関連付けの整合性をとるために、主キー列(
idなど)を必ず SELECT 句に含めるロジックがあります。 - しかし、複合主キーを持つモデル(内部的には
primary_keyが複数列)で、かつselectを明示的に指定している場合、- 必要な複合主キーのカラムが SELECT に含まれない/含め方が不適切
- → eager_load の JOIN 結果を元にレコードを組み立てる処理で不整合・エラーが生じる
という問題 (#57296) がありました。
join_dependency.rb の修正
ActiveRecord::Associations::JoinDependency には、eager load 用クエリに含めるカラムを決定するロジックがあります。
今回の修正は:
- 「CPK モデル + explicit
select」というケースで- そのモデルの すべての主キーカラムを必ず SELECT に含める
- という条件付けが正しく働くよう、条件式を微調整したものです。
イメージとしては、次のような動きをするようになったと考えてください(擬似コード):
# 擬似コードレベルのイメージ
if using_eager_load?
# 以前は単一主キー前提 or 条件が甘く、CPK + select 時に不足
columns_to_select += Array(klass.primary_key) # CPK の場合は ["key1", "key2"] など
endこれにより、たとえば以下のようなケースが正しく動くようになります。
# モデル Post が複合主キー (year, code) を持っているとする
Post.select(:title) # 明示的 select
.eager_load(:comments) # 関連を eager_load
.to_a以前は year や code が SELECT 句に含まれず、eager_load が期待通りのレコード構築を行えないことがありましたが、修正後は内部的に主キー列も自動で SELECT に追加されます。
テストの追加
activerecord/test/cases/associations/eager_test.rb に 11 行のテストが追加されています。内容の趣旨は次の通りです。
- 複合主キーを持つモデルを用意
selectで一部カラムのみを指定しつつeager_loadを使う- 結果がエラーにならず、関連が正しく読み込まれることを検証
このテストが、回 regress しないことを保証する役割を持ちます。
- 影響範囲・注意点
影響範囲
- 対象:
- ActiveRecord で複合主キーを扱っているアプリケーション
- かつ、そのモデルに対して
selectを明示しつつeager_loadを使っているコード
- 具体的な影響:
- 以前は
- エラーが起きる
- もしくは、関連が正しくロードされない/結果が不完全になる
- といった挙動が出ていた箇所が、正常に動作するようになります。
- 内部的には SELECT されるカラムに主キー列(複合主キーの全列)が追加されます。
- 以前は
注意点
- 明示的に
selectを使っていて、「本当にそのカラムしか SELECT したくない」という前提でコードを書いていた場合でも、eager_loadを使う限り、内部的には主キー列が追加で SELECT されます。- これは単一主キーのときも同様の設計思想であり、CPK でもそれに揃えた形です。
- そのため、
- 「SELECT 対象カラムを厳密に制御してパフォーマンスを最適化したい」
- 「SELECT されるカラム数を完全にコントロールしたい」 といったユースケースでは、
eager_loadではなくincludes+references/preloadを使う設計を検討したほうがよい場合があります。
- 参考情報 (あれば)
- 関連 Issue: #57296
- 変更ファイル:
activerecord/lib/active_record/associations/join_dependency.rbactiverecord/test/cases/associations/eager_test.rb
- キーワード:
- Composite Primary Key (CPK)
eager_load- 明示的
select ActiveRecord::Associations::JoinDependency
#57255 Update AGENTS.md to remove PR reference
マージ日: 2026/5/5 | 作成者: @nateberkopec
- 概要 (1-2文で)
このPRは、AGENTS.mdに含まれていた特定のプルリクエスト(PR)への言及を削除する、ドキュメントのみの微修正です。Rails本体の機能や挙動には一切変更がありません。
- 変更内容の詳細
- 対象ファイル:
AGENTS.md - 差分: 1行削除(追加行なし)
AGENTS.md は、Rails 内で利用される「エージェント」に関するドキュメント(例: User-Agent や各種クライアント識別、もしくは内部的な“agent”コンポーネントの解説)をまとめたファイルと考えられます。
今回削除されたのは、その中にあった「特定の PR 番号(あるいは PR への直接リンク)を参照する一文」です。
説明文にある:
Maybe I'm missing something, but this seems way too specific to be intentional.
というコメントから、この PR 参照は意図せず紛れ込んだか、あるいは履歴上のごく一時的な情報をドキュメントに固定してしまっていたため、「ドキュメントとしては不自然なほど具体的すぎる」ので削除した、という判断です。
擬似的なイメージとしては、以下のような行が削除されたと考えられます(実際の文面は異なる可能性があります):
- See PR #12345 for more details about this behavior.あるいは、もっと具体的なケース:
- This behavior was introduced in PR #56789 and should only affect FooBar agents.Rails のガイドライン的にも、ユーザ向けドキュメントに「開発時の個別 PR の履歴」を直接書き込むことはあまり望まれず、安定した概念・API・設定方法に焦点を当てるべきなので、その整理の一環と見てよいです。
- 影響範囲・注意点
- 機能影響: なし
- ソースコード・設定・API などへの変更は一切なく、あくまでドキュメント行削除のみです。
- 互換性への影響: なし
- アプリケーションコードを修正する必要も、Rails のバージョンアップにおける互換性問題も発生しません。
- 開発者視点の注意点:
AGENTS.mdを直接参照していた場合、その中にあった「特定の PR に飛んで詳細を見る」という導線がなくなります。- ただし、その情報は主に内部開発者・コントリビュータ向けの履歴情報であり、通常のアプリケーション開発者にとってはほぼ影響ありません。
- 今後、ドキュメントに履歴的な情報を書く場合は「PR番号を直接書く」よりも、「その機能が何をするのか」「いつから利用可能か(バージョン)」といった抽象度の高い情報にとどめるのが望ましい、という方向性が改めて示されたとも言えます。
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/57255
- Rails のドキュメント方針(参考):
- Guides: https://guides.rubyonrails.org/
- Contributing Guide: https://github.com/rails/rails/blob/main/CONTRIBUTING.md
(ドキュメントにどう書くか・何を書くかの方針が議論されることが多い)
この変更は「クリーンアップ系のドキュメント整理」と捉えてよく、Rails を利用する側としては特別な対応は不要です。
#57280 Fix ActionCable Overview JS example: localStorage.get → getItem
マージ日: 2026/5/5 | 作成者: @Edilbek
概要 (1–2文で)
Action Cableガイド内の JavaScript サンプルコードで、localStorageの誤った呼び出し方 (get) を正しいgetItemに修正したドキュメント変更です。ブラウザでコピペして動く形になるように、動的 WebSocket URL の例を実用的なコードに直しています。変更内容の詳細
対象ファイル: guides/source/action_cable_overview.md の「Connect Consumer」セクション内 getWebSocketURL の例。
もともとのガイドでは (概念的には) こんなコードになっていました:
function getWebSocketURL() {
const token = localStorage.get('auth-token') // ← これが誤り
return `wss://example.com/cable?token=${token}`
}この PR で以下のように修正されています:
function getWebSocketURL() {
const token = localStorage.getItem('auth-token') // ← 正しい Web Storage API
return `wss://example.com/cable?token=${token}`
}技術的背景:
- ブラウザの Web Storage API (
Storageインターフェイス) はgetItem(key: string): string | nullを提供します。 localStorage.getというメソッドは存在しないため、サンプルをそのまま実行するとTypeError: localStorage.get is not a functionが発生します。- これを
getItemに直すことで、Action Cable の接続 URL をlocalStorageのトークンで動的に組み立てる例が、実際にブラウザで動くコードになります。
- 影響範囲・注意点
影響範囲:
- Rails 本体のランタイム動作には一切影響なし (ガイド文書のみの変更)。
- Action Cable の「概要」ガイドを読みながら実装する開発者が、ブラウザ上でエラーに遭遇しなくなる。
注意点:
- すでにドキュメントを参考にして
localStorage.get(...)を書いてしまっているコードがある場合は、手元の実装をgetItem(...)に修正する必要があります。 getItemはキーが存在しない場合nullを返すため、本番コードではnullチェックや未ログイン時のハンドリングを追加することが推奨されます (例: トークンがなければ接続しない、ログイン画面へ誘導するなど)。
- すでにドキュメントを参考にして
- 参考情報 (あれば)
- Web Storage API 仕様:
https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem - 該当ガイド (最新 edge):
https://edgeguides.rubyonrails.org/action_cable_overview.html (「Connect Consumer」セクションの WebSocket URL 例)
#56209 Update Configurable deprecaton warning
マージ日: 2026/5/5 | 作成者: @skipkayhil
- 概要 (1-2文で)
Rails のActiveSupport::Configurableを非推奨にする際に表示される警告文が、「class_attributeを使え」と単一の代替手段だけを示していたのをやめ、複数の代替案を提案する、より中立的な文言に変更した PR です。機能的な変更はなく、あくまで deprecation warning のメッセージ内容のみが更新されています。
- 変更内容の詳細
変更ファイルは 1 行のみで、ActiveSupport::Configurable 使用時に表示される非推奨警告文の文言が修正されています。
背景:
- 以前の PR (ref: https://github.com/rails/rails/pull/53970) で
ActiveSupport::Configurableが非推奨になり、その際の警告文で「class_attributeを使うように」と強く推奨していました。 - その結果、実際には単純な
attr_accessorや別の仕組みで十分なケースでも、ライブラリ作者やアプリ開発者が「とりあえずclass_attribute」を選びがちになる、という誤誘導が発生していた、という問題意識があります。
今回の PR の意図:
- 「
Configurable→class_attributeに置き換えるべき」という “一択” のガイダンスをやめる。 - 用途によっては
attr_accessor等のよりシンプルな代替の方が適切であることを反映し、複数の選択肢を警告文に列挙する。 - これにより、「自分の使い方なら何を使うべきか」を、開発者自身が検討しやすくする。
実際のコード変更のイメージ(擬似例):
# 変更前(イメージ)
ActiveSupport::Deprecation.warn(
"ActiveSupport::Configurable is deprecated. " \
"Use `class_attribute` instead."
)
# 変更後(イメージ)
ActiveSupport::Deprecation.warn(
"ActiveSupport::Configurable is deprecated. " \
"Use an alternative such as `attr_accessor`, `mattr_accessor`, " \
"`class_attribute`, or another configuration pattern as appropriate."
)※上記は説明用の擬似コードです。実際の文言は英語・1 行差分のみですが、趣旨としては「class_attribute 一択 → 複数案提示」に変わっています。
- 影響範囲・注意点
ランタイム挙動:
ActiveSupport::Configurableの機能自体や挙動にはいっさい変更ありません。- 影響があるのは 警告メッセージの内容のみ です。
移行ガイドとしての影響:
- これまで警告に従って機械的に
class_attributeへ置き換えていた場合、それが必ずしもベストプラクティスではないことを再検討すべきケースがあります。 - 特に以下のような場合は、よりシンプルな手段が適している可能性があります:
- 単に「設定値を保持するだけ」のクラスで、クラスインスタンス変数 +
attr_accessorで事足りる。 - クラス階層間での設定の継承やスレッドローカルな設定を必要としていない。
- 単に「設定値を保持するだけ」のクラスで、クラスインスタンス変数 +
- 一方で、以下のような要件がある場合は
class_attribute・mattr_accessorなどを含めた検討が必要です:- サブクラスごとに別々の設定値を持たせたい。
- 設定値をクラスレベルで参照・変更したいが、一定の継承挙動も欲しい。
- これまで警告に従って機械的に
gem・ライブラリ作者への注意:
- 既に「Configurable 非推奨 → class_attribute に一律置換」という対応をしている gem は、その選択が妥当かを再確認してもよいです。
- 今後 Configurable の非推奨警告を見た開発者には、「いくつか選択肢があるので、自分のユースケースに合うものを選ぶ」ことが推奨されます。
- 参考情報
- 元の deprecation PR:
- https://github.com/rails/rails/pull/53970
ActiveSupport::Configurableを非推奨にした変更本体。
- https://github.com/rails/rails/pull/53970
- 今回の PR:
- https://github.com/rails/rails/pull/56209
非推奨メッセージの文言調整のみを行う小規模 PR。
- https://github.com/rails/rails/pull/56209
この PR 自体にはコード上の API 変更や破壊的変更はなく、「移行を促すメッセージの質の改善」が目的です。
#57285 Deactivate ViewReloader hooks when reloaders are cleared
マージ日: 2026/5/4 | 作成者: @ariens
- 概要 (1-2文で)
ViewReloader が登録する「ビューの監視用フック」がapp.reloaders.clear実行後も残り続けて無駄なファイルスキャンを行っていた問題を修正し、リローダー削除時にフックも確実に解除されるようにした PR です。これにより、特に Spring のフォークワーカーなどで、不要なDir[]/File.mtime呼び出しが発生しなくなります。
- 変更内容の詳細
背景
ViewReloaderは、レイルタイ初期化時にPathRegistry.file_system_resolver_hooksにrebuild_watcherフックを登録します(#57269 で導入)。app.reloadersにはViewReloaderなどのリローダーオブジェクトが格納されていて、例えば Spring のテストワーカーなどのフォーク後プロセスではapp.reloaders.clearが呼ばれます。- しかしこれまでは:
app.reloadersの配列からViewReloader自体は削除される- 登録済みの
file_system_resolver_hooks内のフックは生き残る
- 結果として、
prepend_view_pathなどが呼ばれるたびに、もはや必要のないDir.[]+File.mtimeを全ビューディレクトリに対して実行してしまう、という無駄な処理が行われていました。
この PR は「リローダーが Rails から取り除かれるときは、その副作用で登録していたフックも無効化する」という責務を明示化しています。
ViewReloader に #deactivate を追加
actionview/lib/action_view/cache_expiry.rb にある ViewReloader に新しく deactivate メソッドが追加されました。
イメージとしては以下のような処理です(疑似コード):
class ActionView::CacheExpiry::ViewReloader
def initialize(...)
# railtie 側で hook 登録するようになった (#57269)
end
def deactivate
# PathRegistry.file_system_resolver_hooks から
# 自分が登録した hook を取り除く処理
end
endポイント:
ViewReloaderが登録したfile_system_resolver_hooks内のエントリを削除するロジックをカプセル化。- これにより、「リローダーのライフサイクル」と「フックのライフサイクル」が同期される。
app.reloaders の Array を ReloadersCollection に差し替え
railties/lib/rails/application.rb で、これまで単なる Array だった app.reloaders の実体が、ReloadersCollection クラスに差し替えられました。
新クラスは railties/lib/rails/application/reloaders_collection.rb に定義され、主な役割は:
Array互換のインターフェイスを提供しつつ、clear/deleteなどで要素が削除されるときに、その要素に#deactivateがあれば呼び出すこと。
ざっくりしたイメージ:
class Rails::Application::ReloadersCollection
include Enumerable
def initialize
@reloaders = []
end
def <<(reloader)
@reloaders << reloader
end
def clear
@reloaders.each { |r| r.deactivate if r.respond_to?(:deactivate) }
@reloaders.clear
end
def delete(reloader)
if @reloaders.delete(reloader)
reloader.deactivate if reloader.respond_to?(:deactivate)
end
end
# 他にも each / [] / size など Array っぽい API を実装
endこれにより:
- 既存コードは基本的に
app.reloadersを Array として扱い続けられる(<<,each,clearなど)。 - しかし内部的には、
clearやdeleteのタイミングで自動的にdeactivateが呼ばれ、ViewReloaderのフックが確実に外される。 respond_to?(:deactivate)チェックをしているため、ViewReloader以外のリローダー(deactivateを定義していない)には影響しない。
Railtie 側の変更
actionview/lib/action_view/railtie.rb では、#57269 の変更を前提にしたわずかな修正が入っていますが、今回の PR の中心は以下の2点です:
- フック登録の実装の位置は前 PR (#57269) で railtie に移っているため、今回の変更は「登録したフックをどう解除するか」にフォーカス。
ViewReloaderがアプリケーションのリローダー一覧に登録されている前提のもと、その削除時にdeactivateが呼ばれるようになった。
テスト追加
以下のテストが追加されています。
actionview/test/template/cache_expiry_test.rbViewReloaderのdeactivateが実際にfile_system_resolver_hooksからフックを取り除くかを確認するテスト。prepend_view_path時にフックが呼ばれなくなることを検証していると推測されます。
railties/test/application/reloaders_collection_test.rbReloadersCollectionの挙動テスト:clearが各リローダーのdeactivateを呼ぶか。deleteが対象リローダーのdeactivateを呼ぶか。Enumerable的な動作を保っているか。
- 影響範囲・注意点
主に影響を受けるケース
- Spring など、Rails アプリケーションをフォークしてワーカーを立ち上げる環境で、
- フォーク後に
app.reloaders.clearを呼んでいる場合。
- フォーク後に
- これまではフォーク後のワーカーで:
ViewReloader自体は無効化されたつもりでも、PathRegistry.file_system_resolver_hooksのフックだけは生き残り、prepend_view_path(たとえばテストでパスを切り替えるなど)のたびに余計なファイルシステムスキャンが走っていた。
- 今回の変更により:
- その無駄な
Dir[]/File.mtime呼び出しがなくなり、フォークワーカー上でのオーバーヘッドが減る。 - フォーク後のプロセスでファイル監視が不要な典型ケース(テストワーカーなど)で特に有効。
- その無駄な
注意点・後方互換性
app.reloadersは依然として Array ライクなオブジェクトとして振る舞うため、普通に<<,each,clear,deleteをしている分にはコード変更は不要。- ただし、リフレクションや
is_a?(Array)に依存したコードを書いている場合は注意が必要です。- そのようなコードがあれば
ReloadersCollectionを許容する形に直す必要があります。
- そのようなコードがあれば
- 独自リローダーを
app.reloadersに追加していて「削除時に何かクリーンアップしたい」場合:- 任意で
#deactivateを定義しておけば、app.reloaders.clear/delete時に自動で呼ばれるようになります。 - 逆に、そうしたクリーンアップを意図していなければ、特に何もしなくてよい(
respond_to?(:deactivate)で守られている)。
- 任意で
パフォーマンス・動作上の効果
- フォークワーカー上で、不要な view ディレクトリの走査がなくなるため、テスト起動・実行のパフォーマンス向上が見込めます。
- ビューのリロード自体の挙動(開発環境などでの通常のオートリロード)は、今回の PR によって変化しないはずです。
- 影響が出るのは「リローダーを明示的に
clear/deleteしたとき」のみ。
- 影響が出るのは「リローダーを明示的に
- 参考情報 (あれば)
- この PR が前提にしている変更:
- #57269:
ViewReloaderのフック登録をinitializeから railtie へ移動し、watcher の生成を遅延させることでブート時の無駄な再構築を防いだ PR。
- #57269:
- Spring 側の関連 PR:
- https://github.com/rails/spring/pull/754
Spring.after_environment_loadによるアプリケーションレベルのプリロード制御。- 本 PR は Rails 内部のクリーンアップ(
app.reloaders.clearが自動的にフックも無効化する)を担当し、Spring 側のフックとは役割分担されています。
- https://github.com/rails/spring/pull/754
- 変更ログ:
actionview/CHANGELOG.md,railties/CHANGELOG.mdに今回の修正内容の要約が追記されています。
#57289 Raise ArgumentError when MemCacheStore is created with Dalli::Client
マージ日: 2026/5/4 | 作成者: @p8
- 概要 (1-2文で)
Rails のActiveSupport::Cache::MemCacheStoreに、Dalli::Clientインスタンスを渡して初期化しようとした場合にArgumentErrorを明示的に発生させる変更が入りました。これにより、すでに非推奨・削除されていたDalli::Clientベースの初期化方法が、エラーメッセージどおりに完全に禁止されます。
- 変更内容の詳細
対象:activesupport/lib/active_support/cache/mem_cache_store.rb の初期化ロジックが 1 行だけ変更されています。
背景:
- 以前のコミット
78b74e9で「Dalli::ClientをMemCacheStoreに渡す」方法は非推奨となっていました。 - さらに
c33e2d2e49dでそのサポートが削除されています。 - しかし、コード上・エラーメッセージ上は「空配列・アドレス文字列・アドレス配列のみ許可」となっているにもかかわらず、
Dalli::Clientを渡したときに明示的なArgumentErrorを投げていませんでした。
今回のPRでは、この不整合を解消し、Dalli::Client が渡された場合は必ず ArgumentError を投げるようにしています。
イメージとしては以下のようなチェックが入っていると考えてください(実際のコードは 1 行変更のみですが、概念的にはこのような条件分岐です):
# 例: MemCacheStore のコンストラクタ内のイメージ
def initialize(addresses = nil, **options)
# 許可されるのは:
# - nil または空配列
# - "localhost:11211" のような address 文字列
# - ["host1:11211", "host2:11211"] のようなアドレス配列
#
# Dalli::Client は明示的にエラーにする
if addresses.is_a?(Dalli::Client)
raise ArgumentError,
"MemCacheStore: addresses must be empty array, address string, or array of addresses (Dalli::Client is no longer supported)"
end
# ... 残りの初期化処理 ...
endPR 説明にある通り、エラーメッセージの仕様
only "empty array, address, or array of addresses" are allowed
と実際の挙動を揃えるのが主目的です。
- 影響範囲・注意点
影響があるケース:
- アプリケーションやライブラリで、以下のように
Dalli::Clientオブジェクトを直接渡してMemCacheStoreを作っている場合:
# 以前(非推奨だが技術的には動いていたパターン)
client = Dalli::Client.new("localhost:11211")
Rails.application.config.cache_store = :mem_cache_store, clientこの PR 適用後は、上記のようなコードは ArgumentError で落ちます。
正しい移行方法(従来から推奨されていた書き方):
MemCacheStoreには アドレス文字列またはアドレス配列 を渡してください。
# 推奨パターン
Rails.application.config.cache_store = :mem_cache_store, "localhost:11211"
# 複数サーバー
Rails.application.config.cache_store = :mem_cache_store,
["cache1.example.com:11211", "cache2.example.com:11211"]- Dalli を直接使いたい場合は、
MemCacheStore経由ではなく、自前で Dalli をラップする、あるいは Dalli 専用の設定パス(gem・ミドルウェアなど)を利用する必要があります。
注意点:
- この変更は後方互換性を壊す可能性がありますが、
- そもそも過去のコミットで非推奨になっていた
- 対応するエラーメッセージも既に存在していた
という経緯から、「非推奨状態からの明示的なエラー化」という整理にあたります。
- テストと CHANGELOG が更新されているため、この挙動は Rails の正式な仕様変更として扱われます。
- 参考情報 (あれば)
- このPR:
Raise ArgumentError when MemCacheStore is created with Dalli::Client(#57289) - 関連コミット(PR本文で言及されているもの)
78b74e9:Dalli::Clientを渡す方法の非推奨化c33e2d2e49d:Dalli::Clientサポートの削除
MemCacheStoreのサポートする引数は、公式ドキュメントおよびエラーメッセージ上、- 空配列
- 単一のアドレス文字列
- アドレス文字列の配列
に限定される仕様になっています。
#57291 [ci skip] Fix typos in test descriptions
マージ日: 2026/5/2 | 作成者: @jackbowlin
- 概要 (1-2文で)
このPRは、テストコード内の説明文字列(テスト名)のスペルミスを修正したものです。テストの挙動やアプリケーションコードには一切変更はなく、影響は極めて限定的です。
- 変更内容の詳細
対象は以下2つのテストファイルです。
activesupport/test/env_configuration_test.rbactivestorage/test/models/named_variant_test.rb
修正されたのは「テストの説明文字列(テスト名)」だけで、テスト内容(アサーションや実行ロジック)は変更されていません。
ActiveSupport::EnvConfigurationTest
誤字:
reqiure→ 正:require
例(イメージ):
# 変更前(イメージ)
test "env configuration reqiure something" do
# ...
end
# 変更後(イメージ)
test "env configuration require something" do
# ...
end実際には "reqiure" という文字列が "require" に修正されています。
ActiveStorage::NamedVariantTest
誤字:
explicity→ 正:explicitly
例(イメージ):
# 変更前(イメージ)
test "named variants are applied explicity" do
# ...
end
# 変更後(イメージ)
test "named variants are applied explicitly" do
# ...
endこちらも "explicity" が "explicitly" に修正されたのみです。
テスト実行について
PRの説明どおり、「テストの名前(説明)」しか変えていないため、テストは実行されていません。Ruby/Railsのテストランナーは通常、test "..." do の文字列を人間向けの説明として扱うだけで、テストの実行ロジックには影響しません。
- 影響範囲・注意点
テスト・本番動作への影響
- テストの実行結果や挙動、本番コードの動作には影響ありません。
- スナップショットツールや外部のテストレポートに「テスト名で紐づけている」場合、名称の変更として認識される可能性があります(例: テストレポートビューアでのテストケース名が少し変わる程度)。
テスト選択・フィルタリングへの影響
- Railsの
ruby -Itest test/path/to/file_test.rb -n "..."のように、テスト説明文字列を指定してフィルタリングしている場合、旧スペルで指定しているスクリプトはマッチしなくなる可能性があります。 - そのような用途で使っている場合は、新しいテスト名(正しいスペル)に更新する必要があります。
- Railsの
リファクタリング観点
- 本PRは技術的負債の解消というよりは、テストのメタデータの整備に近い変更です。
- このような誤字修正は、ドキュメント生成・テストレポート・検索性(grepなど)におけるノイズ削減に貢献します。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57291
- 変更ファイル:
activestorage/test/models/named_variant_test.rb(+3/-3)activesupport/test/env_configuration_test.rb(+2/-2)
いずれも文字列の修正のみで、テストケースの構造やアサーションロジックの変更はありません。
#57269 Defer ViewReloader build when no view paths are registered
マージ日: 2026/5/1 | 作成者: @Korri
- 概要 (1-2文で)
View 用のファイル更新監視(ViewReloader / FileUpdateChecker)の生成タイミングをさらに遅延させ、「まだ view path が1つも登録されていない状態」でupdated?が呼ばれた場合でも不要な watcher を作らないようにした PRです。これにより、特に Spring 利用時のテスト実行など、開発環境での起動〜初回リクエスト前の無駄なディレクトリ走査が大幅に削減されます。
- 変更内容の詳細
背景と問題点
前回の変更 (#51308) で、「最初の updated? チェックまでは View watcher を作らない(lazy build)」という仕組みが導入されていました。しかし、以下のケースで問題が残っていました。
dirs_to_watch(監視対象ディレクトリの配列)が まだ空の状態 でupdated?が呼ばれる- その時点で
#build_watcherが呼ばれ、中身が空のFileUpdateCheckerでもとりあえず作成して@watcherにセットしてしまう - 以降は
@watcherが non-nil なので#rebuild_watcherのreturn unless @watcherガードが常に通り、
後から各エンジンがprepend_view_pathするたびに「全ディレクトリ集合に対するDir[*globs] + File.mtimeのフルスキャン」が走る
Spring を使ったテスト環境ではこれが顕在化します。
- Spring の preloader は
Rails.application.reloadersを直接見てreload!が必要か判断します(Reloader.check!は通さない) - 多くのエンジンの
prepend_view_pathはActiveSupport.on_load(:action_mailer)/:action_controller_baseのフック内で行われるため、preload フェーズではまだ呼ばれていない - そのため「view path が1つも登録されていない状態」で
updated?が呼ばれ、空 watcher が固定されてしまう - 結果として、その後の各
prepend_view_pathごとにフルスキャンが走り、フォークごとに高コストなディスク I/O が発生
PR の説明中にある挙動比較は以下の通りです。
修正前
- 早期ブート時の
updated?呼び出し- → watcher が空のまま作られる(軽いが「作ってしまう」)
- その後の
prepend_view_path×10- → 10回ともフル再構築(ディレクトリ再走査)
- 最初のリクエスト時の
updated?- → すでにウォッチャーがあるので再構築なし
修正後
- 早期ブート時の
updated?呼び出し- → view path が空なので watcher を作らない
- その後の
prepend_view_path×10- →
@watcherがまだ nil のため 0回再構築
- →
- 最初のリクエスト時の
updated?- → ここで初めて watcher を build(1回だけディレクトリ走査)
実際のコード上の変更点(概念的な説明)
※実際の diff からの要約であり、擬似コードを含みます。
1) ActionView::CacheExpiry 内の watcher ビルドロジック
actionview/lib/action_view/cache_expiry.rb の主な変更点は:
build_watcher/rebuild_watcher周りの条件分岐が見直され、- 「監視対象ディレクトリが空のときは watcher を作らない」 というガードが追加された
イメージとしては以下のような流れになります(擬似コード):
def build_watcher
return if dirs_to_watch.empty? # 追加されたガード: 何も監視対象がなければ何もしない
@watcher = ActiveSupport::FileUpdateChecker.new(dirs_to_watch, &callback)
end
def rebuild_watcher
return unless @watcher # 既存のガード: watcher がないなら何もしない
return if dirs_to_watch.empty? # 追加(もしくは強化)された条件: 空なら再構築もしない
@watcher = @watcher.rebuild(dirs_to_watch)
endポイント:
- 初回
updated?@watcherが nil かつdirs_to_watchが空 →build_watcherしても何も作らない →@watcherは nil のまま
- view path が登録されたあと
- 初めて
dirs_to_watchが非空の状態でupdated?が走ったタイミングで watcher を生成
- 初めて
- その後のパス追加
- すでに
@watcherがある状態でdirs_to_watchが変化したときにだけrebuild_watcherが動く
- すでに
これにより「空の watcher が早期に固定される」パターンが潰され、prepend_view_path のたびにフルスキャンが走るのを防いでいます。
2) Railtie 側の微調整
actionview/lib/action_view/railtie.rb では 1 行だけの追加ですが、ViewReloader の登録や初期化タイミングに関する配慮が行われています。
- ねらいとしては、「reloader の初期化そのものは行うが、実際の FileUpdateChecker の生成は view path が揃うまで遅延させる」という設計に沿うようになっています。
3) テストの追加
actionview/test/template/cache_expiry_test.rb に約 94 行のテストが追加されています。
主に以下を確認する内容と考えられます。
- view path 未登録時に
updated?を呼んでも watcher が作られない こと - その後に
prepend_view_pathを複数回呼んでも、不必要に watcher を再構築しないこと - 最初に「非空の view path セット」で
updated?を呼んだときにのみ watcher が1回だけ作られ、その後の挙動が正しいこと
これにより、今回の遅延ロジックが将来的に regress しないように自動テストでカバーしています。
4) CHANGELOG
actionview/CHANGELOG.md に今回の修正の概要が追記され、Action View の挙動変更として明示されました。
- 影響範囲・注意点
影響範囲
- 対象:
- ActionView の ViewReloader / CacheExpiry 周り
- 主に 開発環境・Spring 利用時 に影響
- 影響しないケース:
- production 等で
cache_classes = trueの場合
→ ViewReloader 自体がスキップされるため、挙動は従来通り - 「最初の reloader チェック(
updated?)よりも先に、全ての view path が登録される」ような小規模アプリ
→ もともと問題が顕在化しないケースなので挙動上の差はほぼない
- production 等で
性能面のインパクト
- Shopify の Rails モノレポでの計測では、Spring を使ったテストループ(小さめのワークロード)で
- 5.16 秒 → 3.66 秒(約 29% の短縮)
- 実際には、
prepend_view_pathを多用している大規模アプリケーションほど恩恵が大きくなります。
互換性・注意点
- 監視対象ディレクトリが「存在しない or 空」である場合には watcher が作られない挙動になりますが、
- 「view path が 1 つも登録されていないのに view キャッシュの更新検知をしようとする」というケースはそもそも意味がないため、実質的には妥当な制約です。
- 独自に
ActionView::CacheExpiryを拡張しているような特殊なコードベースがある場合は、@watcherが nil であり続けるパターンが増えることを前提にしているかどうかを確認した方が安全です。
- view path の追加タイミングに依存して「最初の
updated?時に必ず watcher がある」と仮定しているコードがあると、
その前提が崩れる可能性はありますが、通常の Rails アプリではまず問題にならない想定です。
- 参考情報 (あれば)
- 本 PR:
- Defer ViewReloader build when no view paths are registered (#57269)
- 直前の関連 PR:
- Do not build View watcher until the first
updated?check (#51308)
- Do not build View watcher until the first
- コード参照:
actionview/lib/action_view/cache_expiry.rb- View の更新検知周り(
build_watcher,rebuild_watcher,updated?など)
- View の更新検知周り(
actionview/lib/action_view/railtie.rb- ViewReloader の登録・初期化
actionview/test/template/cache_expiry_test.rb- 遅延ビルドと再構築ロジックのテスト
- Spring 側の関連コード(参考):
spring/lib/spring/application.rb:197Rails.application.reloadersを直接見て reload が必要か判定するロジック
#57283 Avoid assert_nil for Mime::NullType request format
マージ日: 2026/5/1 | 作成者: @mugitti9
- 概要 (1-2文で)
Railsのテストコードで、未知のフォーマット要求時のrequest.formatをassert_nilで検証していた箇所を、Mime::NullTypeを前提とした検証に変更し、最新の Minitest の挙動変更に追従したテスト専用の修正です。実行時の挙動や公開APIには影響せず、テストの互換性・堅牢性を高めるための変更です。
- 変更内容の詳細
背景となる問題
- Rails では、未知のフォーマットがリクエストされた場合、
request.formatはnilではなくMime::NullTypeのインスタンスを返します。 - もともとテストでは、このケースを
assert_nil request.formatのように「nil であること」で検証していました。 - しかし Minitest 側の変更(
assert_nilがobj.nil?ではなくnil == objで比較するようになった)により、- 以前:
obj.nil?を呼ぶ →Mime::NullType#nil?がtrueを返せばテストが通る - 現在:
nil == objを評価 → どんなにobj.nil?がtrueでも、objが実際にnilでないため失敗
という不整合が発生していました。
- 以前:
このPRでの主な修正ポイント
1) request.format のテストを Mime::NullType 前提に変更
actionpack/test/dispatch/request_test.rb のテストが、以下のような形に修正されています(擬似コード):
format = request.format
# 以前:
# assert_nil format
# 以後: 「NullType であること」と「nil? が true であること」を明示的に検証
assert_instance_of Mime::NullType, format
assert_predicate format, :nil?
# 既存の html?/xml?/json? などの否定チェックも維持
refute_predicate format, :html?
refute_predicate format, :xml?
refute_predicate format, :json?ポイント:
- 単に「nil と等しいか」ではなく、「
Mime::NullTypeというクラスのオブジェクトであり、その#nil?がtrueを返す」という Rails 独自の仕様を明示的にテスト。 - すでに存在していた
html?,xml?,json?などのフォーマット判定メソッドのテストは維持され、Unknown フォーマット時にはどれもfalseであることを引き続き確認しています。
2) actiontext 側のテストも同様に修正
actiontext/test/unit/model_test.rb にも同種の assert_nil を使ったテストがあり、同じ問題が起きる可能性があるため、こちらも同じ方針で Mime::NullType + nil? の検証に書き換えられています。
3) Gemfile.lock の微調整
Gemfile.lock に数行の変更 (+3/-2) が入っており、Minitest を含むテスト関連依存のバージョンが更新されています(今回の assert_nil の仕様変更を取り込んだバージョンに追従するためのものと考えられます)。
本体コードではなくテスト環境に関連する変更です。
- 影響範囲・注意点
実行時の Rails コード (
Mime::Type,Mime::NullType,ActionDispatch::Requestなど) の挙動は変わりません。あくまでテストコードのみの変更です。ただし、以下のようなプロジェクトには実務的な示唆があります:
Minitest を新しいバージョンに上げた Rails アプリ
- 自前のテストで
assert_nil some_request.formatのように「nil?に依存したテスト」を書いている場合、今回と同じ理由で壊れる可能性があります。 - 未知フォーマットで
Mime::NullTypeを返す箇所に対しては、assert_instance_of Mime::NullType, objassert_predicate obj, :nil?
など、Rails の実際の戻り値仕様に沿ったテストに修正するのが望ましいです。
- 自前のテストで
nil?をオーバーライドしているクラスに対するassert_nil- Minitest の仕様変更により、「
nil?をオーバーライドした擬似 Nil オブジェクト」をassert_nilでテストする手法は通用しなくなります。 - そのようなオブジェクトについては、このPRと同様に
- 型 (
assert_instance_of/assert_kind_of) - ふるまい (
assert_predicate obj, :nil?) で個別に検証するスタイルに切り替える必要があります。
- 型 (
- Minitest の仕様変更により、「
本PRはテストの期待値を Minitest 実装依存から切り離し、「Rails 側が本来保証したい仕様(NullType +
nil?)」を直接テストする形に整理しているため、長期的にはテストの安定性が向上します。
- 参考情報 (あれば)
- 関連する Minitest の変更:
assert_nil(obj)が内部でobj.nil?を呼ぶ実装から、nil == objで比較する実装に変更されたコミット:minitest/minitest@c471347
- Rails の
Mime::NullType仕様:- 未知の MIME タイプに対するフォーマットオブジェクトであり、
nil?をtrueにすることで「nil に準ずる」扱いを可能にしているが、オブジェクトとしてはnilとは異なる、という設計になっています。
- 未知の MIME タイプに対するフォーマットオブジェクトであり、