Ruby on Rails PR Digest - 2026年 4月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#57176 Make becomes preserve marked_for_destruction
マージ日: 2026/4/29 | 作成者: @genezys
- 概要 (1-2文で)
ActiveRecord::Persistence#becomesを使用してオブジェクトを別クラスのインスタンスに変換した際に、
marked_for_destructionの状態- ロード済み関連(associations)上の変更
が失われないようにするバグ修正・挙動改善の PR です。
- 変更内容の詳細(あればサンプルコードも含めて)
何が問題だったか
becomes は、同じテーブルを共有する STI などの文脈で、あるモデルインスタンスを別クラスのインスタンスに「なり替える」ためのメソッドです。
従来の挙動では、以下のようなケースで問題がありました:
- 親オブジェクトの
has_many/has_one関連で、子レコードをmarked_for_destructionにしていた場合 - 関連オブジェクトに対して変更を行っていた場合
becomes によって新しいクラスのインスタンスへ変換すると、その
marked_for_destructionフラグ- ロード済み関連オブジェクト上の変更
が新インスタンスに正しく引き継がれないことがあった、というのが本 PR のモチベーションです。
具体的な修正点
ActiveRecord::Persistence#becomes に次の2点の「状態の引き継ぎ」が追加されました。
marked_for_destructionの引き継ぎ
すでにpreviously_new_recordについては引き継いでいましたが、それと同様の扱いで- 元のインスタンスで
marked_for_destruction? == trueであれば becomesで生成された新インスタンスにも同じ状態をコピー
するように修正されています。
- 元のインスタンスで
ロード済み関連(associations)の引き継ぎ
- すでにロード済み(= association キャッシュに載っている)関連のみを対象に
- 新インスタンスの association キャッシュにも同じ関連オブジェクトを設定
するようになりました。
これにより、becomes前に関連に対して行っていた変更(破棄マークや属性変更など)が、新インスタンス側でも失われません。
重要なのは「キャッシュ済みの関連のみ」を対象にしている点です。
これによって:- 未使用の関連をわざわざロードしない(パフォーマンスと意図しない副作用を防ぐ)
- テストにある「不正な関連定義」(ロードしたらエラーになるような association)に触れずに済む
といった利点があります。
簡単なサンプルイメージ
(PR 本文には具体的コード例はありませんが、挙動イメージとして)
class Person < ApplicationRecord
has_many :pets
end
class Admin < Person
end
person = Person.find(1)
person.pets.first.mark_for_destruction
admin = person.becomes(Admin)
# 修正前:
admin.pets.first.marked_for_destruction? # => false (フラグが失われる可能性)
# 修正後:
admin.pets.first.marked_for_destruction? # => true (元の状態が保持される)このように becomes 後も、関連に対して行った「破棄予定」やその他の変更が保たれることを意図しています。
- 影響範囲・注意点
- 対象:
ActiveRecord::Persistence#becomesを利用しているコード全般- 特に、STI や同一テーブルを共有するクラス間での「型の切り替え」と、mass-assignment を組み合わせて使っている箇所
- 期待される影響:
- これまで
becomesの後に、子レコードの削除マークや関連オブジェクトの変更が「なぜか消えている/反映されない」といったバグが発生していた場合、その挙動が修正されます。 - 逆に言えば、古い挙動(
becomesしたらフラグがリセットされることを前提としたコード)が存在していた場合は、その前提が崩れます。ただし、そのような前提は通常はバグ寄りの設計と考えられます。
- これまで
- パフォーマンス面:
- 変更は「キャッシュ済みの関連」に対してのみ行うため、新たに関連をロードすることはなく、既存コードに対して大きなパフォーマンス悪化は想定されません。
- テスト:
activerecord/test/cases/persistence_test.rbにテストが追加されており、marked_for_destructionが保持されること- ロード済み関連の変更が保持されること
が自動テストで担保されています。
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::Persistence#becomes - 関連する状態:
marked_for_destruction(nested attributes などで使われる削除フラグ)previously_new_record(すでにbecomesで引き継がれていた状態)
- ファイル変更:
activerecord/lib/active_record/persistence.rbbecomesの実装に、状態・関連の引き継ぎロジックを 1 行追加activerecord/test/cases/persistence_test.rb
状態保持を検証するテストを 9 行追加
#57252 Enable frozen string literal by default
マージ日: 2026/4/27 | 作成者: @byroot
- 概要 (1-2文で)
Rails が生成する新規アプリケーションで、「文字列リテラルの凍結(frozen_string_literal)」をデフォルト有効にする変更です。影響はアプリ自身のコードに限定され、Gem などの依存ライブラリにはデフォルトでは適用されません。
- 変更内容の詳細
※PR本文・差分から読み取れる範囲での要約です。
2-1. 新規アプリで frozen string literal がデフォルト有効に
この PR では、新しく rails new で生成されるアプリケーションについて、Ruby の「文字列リテラルをデフォルトで凍結する」設定を有効にしています。
実現方法としては、以下のような変更が組み合わさっています(実ファイル名ベースで整理):
config/bootsnap.rb.tt の変更
bootsnap の設定ファイルテンプレートに、アプリの自前コードを対象として frozen_string_literal を有効化する設定が追加されています。
イメージとしては、Bootsnap.setup のオプションに「自分のアプリのコードは frozen string literal モードで解釈する」旨の設定が入る形です。
# config/bootsnap.rb (生成イメージの一例)
Bootsnap.setup(
# 中略
load_path_cache: true,
compile_cache_iseq: true,
compile_cache_yaml: true,
# アプリケーションコードに対して frozen_string_literal を有効化
# (実際のキー名は bootsnap の実装側に依存)
allow_frozen_string_literal: true
)※実際のキー名や詳細は、参照 PR(bootsnap #535)側の API に沿ったものになります。
Rails 自体は Ruby のソースコードを直接パースして magic comment を書き換えるわけではなく、bootsnap の ISeq キャッシュ機能を利用して、アプリコードを frozen string モードで実行するようにしています。
Gemfile.tt の変更
Gemfile のテンプレートも 1 行だけ変更されています。
内容としては、bootsnap のバージョン / 設定を、上記の frozen string 対応を使えるものに合わせて更新している可能性が高いです(古い bootsnap ではこの機能がないため)。
# 例 (ニュアンスのみ)
gem "bootsnap", require: false
# → ここが frozen string 対応可能なバージョンを指すように変更rubocop.yml.tt の変更
生成されるデフォルトの .rubocop.yml に、Style/FrozenStringLiteralComment など、frozen string 周りの設定が追記されています。
これにより、Rubocop も「frozen string が前提」のスタイルガイドで動くようになります。
例(イメージ):
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always # など(実際の値は差分に依存)ただし、この PR は magic comment を各ファイルに自動で付与するのではなく、bootsnap による実行時の設定で凍結を有効化するため、「Rubocop は magic comment を期待するが、実際には bootsnap で有効化されている」という状態になる可能性があります。
そのため Rubocop 設定側で「コメントを要求しない」ようなコンフィグが入っている可能性もあります。
app_generator.rb / app_generator_test.rb の変更
app_generator.rb
新規アプリ作成時に、上記 bootsnap / rubocop / Gemfile などのテンプレート変更が反映されるように 1 行追加されています(オプションの有効化やテンプレートコンテキストの調整など)。app_generator_test.rb
生成されたアプリに frozen string 設定が正しく反映されることを検証するテストが追加されています。
例としては「config/bootsnap.rbなどに特定の設定が書き込まれているか」などを確認しているはずです。
CHANGELOG.md の追記
Rails 7.x / 8.x(どのバージョンかは対象ブランチによる)向け CHANGELOG に、以下のような項目が追記されています:
- 新規アプリケーションでは frozen string literal がデフォルトで有効になること
- 影響はアプリケーションコードに限られること
- Gems などの依存コードについては、デフォルトでは frozen string を強制しないこと
- 影響範囲・注意点
3-1. 影響範囲
- 対象:この PR が取り込まれた Rails バージョン以降で
rails newしたアプリの「アプリケーションコード」app/models,app/controllers,app/views,config/など、いわゆる自前コード
- 非対象:Gem やプラグインなどの外部依存コード
- 既存の gem の中には、文字列リテラルが凍結されると壊れるものがまだあり得るため、デフォルトでは適用されない
3-2. 起きうる問題
自分のアプリコード内で、以下のような書き方をしていると、FrozenError が発生する可能性があります。
# NG 例(frozen string 下ではエラー)
foo = "hello"
foo << " world" # "can't modify frozen String: \"hello\"" のようなエラー
# 改善例
foo = +"hello" # dup して非 frozen にする
foo << " world"
# あるいは
foo = "hello world"頻出しがちなパターン:
- 定数内の文字列をミューテートする
- メソッド引数で渡ってきた文字列に対して破壊的メソッド (
<<,gsub!,upcase!など) を直接呼ぶ String#freezeを前提にした一部の最適化コードの挙動が変わる
特に、古いチューニングコードや、曖昧な破壊的操作が多いコードベースでは、テストを回すとエラーがあぶり出される可能性があります。
3-3. 既存アプリへの影響
- 既存アプリには「自動的には」適用されません。
- ただし、この PR に合わせて自分で
config/bootsnap.rbを編集し、同様の設定を追加すれば、既存アプリでも同じモードを有効化できます。 - その場合は、テストをフルに回して
FrozenErrorが出ないかを確認することが推奨されます。
3-4. gems への適用について
PR 説明にある通り:
It is also possible to enable it for gems, but some old ones may still not be ready.
bootsnap の設定次第で、Gem のコードにも frozen string を適用することは技術的には可能です。
しかし、古い Gem の中にはまだ非対応なものがあるため、Rails はデフォルトではそこまで踏み込んでいません。
もし gem にも適用したい場合は、段階的にオンにして、問題があれば Gem 側のアップデートや修正を検討する必要があります。
- 参考情報 (あれば)
- 参照 PR: https://github.com/rails/bootsnap/pull/535
- bootsnap 側で「frozen string literal を有効にして ISeq キャッシュを扱う」機能を追加している PR です。
- Ruby 本体の機能:
# frozen_string_literal: true- ファイル先頭のマジックコメントとしておなじみですが、この PR ではコメントベースではなく bootsnap ベースで一括制御しているのがポイントです。
- 一般的な対応指針:
- 文字列を変更したいときは
dupあるいは+"literal"を使って非 frozen な文字列を生成する - グローバル/定数の文字列は基本的に不変オブジェクトとみなし、破壊的変更を避ける
- Rubocop など静的解析ツールの設定を frozen string 前提(コメント必須 or コメント不要)に揃えることで、スタイルと挙動の齟齬を抑える
- 文字列を変更したいときは
#56718 Fix innermost constraint reseting previous constraints
マージ日: 2026/4/27 | 作成者: @petrenkorf
- 概要 (1-2文で)
ActionDispatch::Routingの「constraints」のマージ処理が修正され、ネストした複数の constraint を定義した際に「一番内側だけが有効になる」不具合が解消されました。これにより、ネストされた constraints ブロックがすべて評価され、期待通りに複合条件として動作するようになります。
- 変更内容の詳細
問題の背景
Rails のルーティングでは、以下のように constraints をネストして使うことができます:
constraints ConstraintA.new do
constraints ConstraintB.new do
get "/foo", to: "foo#index"
end
end本来であれば、ConstraintA と ConstraintB の両方が満たされたときだけ /foo にマッチしてほしいところですが、バグにより「一番内側 (ConstraintB) だけが評価される」状態になっていました。
つまり、外側の constraints が内側の constraints によって上書き(リセット)されてしまう挙動になっていました。
修正のポイント
この PR では、ActionDispatch 内部の「constraints のマージ処理」を変更し、単一の constraint を都度上書きするのではなく、constraint ブロックのリストとして蓄積し、順番に評価する 形に修正しています。
コード上では actionpack/lib/action_dispatch/routing/mapper.rb で以下のような変更が行われています(疑似イメージ):
これまでのイメージ(問題のある挙動)
# 擬似コードイメージ
def merge_constraints(existing, new_constraint)
# new_constraint があれば既存を上書きしてしまう
new_constraint || existing
endこのため、ネストしたときに一番内側の constraint が最終的な constraint として採用されてしまっていました。
修正後のイメージ
# 擬似コードイメージ
def merge_constraints(existing, new_constraint)
# constraint を配列として扱い、どんどん蓄積する
Array(existing) + Array(new_constraint)
end
# ルーティング評価時には、蓄積された全 constraint を順次評価
def matches?(request)
constraints.all? { |c| c.matches?(request) }
end実際にはもう少し複雑ですが、概ね以下のような方針です:
constraintsオプション/ブロックを「単一のオブジェクト」ではなく「複数の constraint を表すコレクション」として扱うように変更- ネスト時の merge で「上書き」ではなく「配列への追加」になるよう変更
- ルーティングのマッチ判定時に、その配列内のすべての constraint を評価して AND 条件を実現
テストの追加
actionpack/test/dispatch/routing_test.rb に新しいテストが追加されています(+13 行)。
内容としては、以下のようなケースをカバーしていると考えられます:
- 外側と内側でそれぞれ別の constraint を定義
- 両方が満たされる場合にのみルートがマッチすること
- 一方だけ満たされない場合にマッチしないこと
これにより、以前報告されていた Issue (#56528) の再発を防ぐ形になっています。
- 影響範囲・注意点
影響範囲
constraintsをネストしているルーティング定義に挙動の変化があります。- これまでバグに依存して「内側だけが効く」ことを前提にしていた場合、期待挙動が変わる可能性がありますが、本来の仕様に近づく方向の変更です。
- 通常の(非ネスト)constraints の使い方にはほとんど影響ありません。
既存アプリでの確認ポイント
routes.rbで以下のようなパターンを使っている場合は、テストで挙動確認をしておくとよいです:rubyconstraints ConstraintA do constraints ConstraintB do # routes... end end- 特に、IP 制限やサブドメイン制御、認証状態など複数条件を constraint で表現している場合、全ての constraints が AND 条件で効くようになっていることを前提にテストを見直すと安心です。
パフォーマンス
- constraints がリスト化されることで、ネストが深い場合に評価回数が増えますが、通常の数であれば無視できる程度です。
- 非常に多数の constraints をネストしている場合は、ルーティングマッチのコストがわずかに増える可能性があります。
- 参考情報 (あれば)
- 関連 Issue: https://github.com/rails/rails/issues/56528
- ネストした constraints で「一番内側しか効かない」問題の報告・議論。
- この PR: https://github.com/rails/rails/pull/56718
- 変更された実際のコードとテストの詳細は上記を参照してください。
#57238 Remove redundant libvips from Dockerfile build packages
マージ日: 2026/4/27 | 作成者: @rikki3
- 概要 (1-2文で)
Rails が生成する Dockerfile から、ビルドステージ側に重複して記述されていたlibvipsのインストールを削除した PRです。ベースイメージ側ですでにlibvipsを含んでいるため、不要な二重インストールを解消しています。
- 変更内容の詳細
対象ファイル: railties/lib/rails/generators/app_base.rb
変更行数: 追加 0 / 削除 3
rails new でアプリケーションを生成する際に使われるテンプレートから、Dockerfile の「build 用パッケージ」リストに含まれていた libvips を削除しています。
背景として説明にある通り:
- Rails の生成する Dockerfile には
- ベースイメージ(runtime)用のパッケージ群
- ビルドステージ(build)用のパッケージ群
という 2 種類のインストールリストがあり、
- build ステージは base ステージを継承しているため、base に入っているパッケージは build にもすでに入っている
- にもかかわらず、両方に
libvipsが書かれていたため、Dockerfile 上で「同じパッケージを二度指定している」状態だった
この PR では「build packages 側に書かれていた libvips を削除」することで、Dockerfile 生成時に libvips のインストール行が重複しないようにしています。
疑似的なイメージ:
# 変更前(概念的なイメージ)
build_packages = %w[
build-essential
libvips # <- base 側にも入っているのにここにもあった
...
]
# 変更後
build_packages = %w[
build-essential
# libvips は削除
...
]実際の diff は 3 行削除のみで、新しいパッケージの追加やロジック変更はありません。
- 影響範囲・注意点
- 対象:
- この PR マージ後の Rails バージョンで
rails newした際に生成される Dockerfile - とくに、
libvipsを利用している Active Storage + image_processing などの構成で Docker を使う場合
- この PR マージ後の Rails バージョンで
- 動作面:
libvips自体は引き続きベースイメージ側のパッケージとしてインストールされるため、ランタイムでの画像処理等の機能には影響しません。- build ステージも base を継承しているので、ビルド時に
libvipsが足りなくなることもありません。
- 影響:
- Dockerfile 上の冗長なインストール指定がなくなるだけであり、機能追加・削除はなし。
- Docker のレイヤーキャッシュやログから見えるパッケージインストール行が少しシンプルになります。
- 注意点:
- 既存プロジェクトの Dockerfile を手書き・カスタマイズしている場合には自動的には変わりません。重複インストールが気になる場合は、同様に build 用パッケージから
libvipsを削ることを検討すると良いです。 - ベースイメージを独自に差し替えていて、そちらに
libvipsを含めていない場合は、この PR の想定と異なる構成になるため、自前 Dockerfile では明示的にインストールする必要があります(ただしこの PR は Rails が生成する標準 Dockerfile テンプレートのみを変更します)。
- 既存プロジェクトの Dockerfile を手書き・カスタマイズしている場合には自動的には変わりません。重複インストールが気になる場合は、同様に build 用パッケージから
- 参考情報 (あれば)
- 対応 issue: #57237
libvipsは Active Storage +image_processinggem などで用いられる高速な画像処理ライブラリです。- 該当テンプレートは Rails アプリ作成時のジェネレータ (
Rails::Generators::AppBase) にあり、--dockerなどのオプションで Dockerfile を生成する際に利用されます。
#57246 Optimize ActiveRecord::Relation#extending! to skip work when called with empty modules
マージ日: 2026/4/26 | 作成者: @bogdan
- 概要 (1-2文で)
ActiveRecord::Relation#extending!が、空のモジュール配列やブロックなしで呼ばれた場合に即座に何もせず return するよう最適化された PR です。これにより、merge_multi_valuesを含むあらゆる呼び出し元で、不要な処理を避けてパフォーマンスを改善しています。
- 変更内容の詳細
背景
ActiveRecord::Relation#extending! は、リレーションに対して拡張モジュールを付与するために使われるメソッドです:
Model.where(...).extending!(SomeExtensionModule)このメソッドは、Relation#extensions 配列にモジュールを追加するなどの処理を行いますが、呼び出し側によっては「空のモジュール配列」を渡すケースもあり、その際にも内部処理が走ってしまうのが無駄でした。
前 PR (#57199) では、merge_multi_values 側にガードを置く対応がされていましたが、この PR ではそれを一歩進めて、「extending! 自体が早期リターンできるようにする」ことで、全ての呼び出し元で最適化の恩恵を受けられるようにしています。
extending! の変更
extending! に以下のようなガードが追加されたと考えられます(意訳コード):
def extending!(*modules, &block)
# 新規: モジュールもブロックもない場合は何もしない
return self if modules.empty? && !block_given?
# もともとの処理(概要)
self.extending_values |= modules # `|=` で重複を除外しつつ追加
extending!(Module.new(&block)) if block_given?
self
endポイント:
- モジュール引数が空 (
modules.empty?) かつブロックもない (!block_given?) 場合は、即座にselfを返す。 - すでに
|=を用いた配列マージで重複除去(deduplication)を行っていたため、呼び出し元側での手動の差集合計算は不要になった。
merge_multi_values の簡素化
merge_multi_values は、リレーション同士のマージ時に extensions もマージするメソッドです。以前は、other.extensions と relation.extensions の差集合を手動で計算してから extending! に渡していました。
従来イメージ(意訳):
# 以前は手動で差集合を計算
extensions_to_add = other.extensions - relation.extensions
relation.extending!(*extensions_to_add) unless extensions_to_add.empty?PR 後のイメージ:
# 差集合の計算をやめ、単に extensions を渡すだけ
relation.extending!(*other.extensions) unless other.extensions.empty?変更点:
a - bによる差集合計算を削除。unless other.extensions.empty?というガードだけ残し、dedup はextending!内部の|=に任せる設計に。- さらに、「空の extensions」の場合にメソッド呼び出し自体をスキップすることで、ホットパスでのオーバーヘッドを抑えている(
extending!側にもガードはあるが、「そもそも呼ばないほうが速い」ので二重ガード)。
- 影響範囲・注意点
- 対象メソッド:
ActiveRecord::Relation#extending!ActiveRecord::Relation::Merger#merge_multi_values
- 機能的な挙動:
- 機能面の変更はほぼなく、「空の extensions / ブロックなし」の場合に何もせず戻るようになっただけです。
extending!が内部で|=を使っているため、モジュールの重複は引き続き自動的に除外されます。
- 互換性:
extending!の戻り値は従来通りselfであり、メソッドチェーンの挙動は変わりません。- 空の引数・ブロックなしで
extending!を呼んでいたコードがあっても、もともと何も有効な効果を持たない呼び出しなので、実質互換性の問題はありません(内部コストが減るだけ)。
- パフォーマンス:
- 空の extensions を頻繁に扱うようなコードや、
mergeによるクエリビルドを多用している箇所で、わずかなパフォーマンス改善が見込まれます。
- 空の extensions を頻繁に扱うようなコードや、
- 参考情報 (あれば)
- 該当 PR:
- Optimize
ActiveRecord::Relation#extending!to skip work when called with empty modules (#57246)
- Optimize
- 関連 PR:
- #57199 (今回の follow-up 元の最適化 PR)
- 重複除去のテスト:
Relation#extending!の dedup 周りの動作は既存テストでカバー済み
https://github.com/rails/rails/blob/main/activerecord/test/cases/relation_test.rb#L45-L62
#54829 Allow using aliases for unions in from clause
マージ日: 2026/4/26 | 作成者: @fatkodima
- 概要 (1-2文で)
Rails のActiveRecord::Relation#fromに対し、UNIONを使ったサブクエリにテーブルエイリアスを付けて利用できるようにする修正です。これにより、from(union, :some_models)のような呼び出しで、PostgreSQL などで発生していた「missing FROM-clause entry」エラーが解消されます。
- 変更内容の詳細
これまでの問題点
以下のように、UNION を使ったクエリを from 経由で使いたいケースを想定します:
union = SomeModel.select(:id).where(...)
.union(OtherModel.select(:id).where(...))
# UNION した結果を from で参照し、エイリアス some_models を付与したい
relation = SomeModel.from(union, :some_models)- Active Record は
from(union)自体は受け付けるものの、 - 第2引数で渡された
:some_modelsのエイリアスを無視して SQL を生成していました。 - そのため、生成される SQL は例えば以下のようになっていたと考えられます:
SELECT "some_models".* FROM (
(SELECT "some_models"."id" FROM "some_models" WHERE ...)
UNION
(SELECT "other_models"."id" FROM "other_models" WHERE ...)
)
-- ※ ここに AS some_models が無いその結果、SELECT "some_models".* ... で参照している "some_models" というテーブル名/エイリアスが FROM 句に存在せず、PostgreSQL で
missing FROM-clause entry for table "some_models"
というエラーが発生していました。
PR の対応内容
ActiveRecord::Relation#from が、引数として渡されたリレーション(特に UNION を含むもの)に対しても、明示的なエイリアスを正しく適用するように修正されています。
ポイント:
from(union_relation, :alias)といった呼び出し時、- これまでは、この
:alias情報が内部で無視されていた。 - 修正後は、生成される SQL の
FROM句にAS aliasが付与されます。
- これまでは、この
おおよそのイメージ:
# 修正後の期待挙動
SomeModel.from(union, :some_models)
# => SQL (イメージ)
# SELECT "some_models".* FROM (
# SELECT ... FROM ...
# UNION
# SELECT ... FROM ...
# ) AS some_modelsテストコード側 (activerecord/test/cases/relations_test.rb) では、
UNIONを使ってRelation#unionで組み立てたサブクエリをfromに渡す- その際に別名(エイリアス)を渡し、それが実際に SQL に反映される
といった内容のテストが追加されています。
これにより、今回の挙動が回帰しないよう担保されています。
- 影響範囲・注意点
- 影響範囲:
ActiveRecord::Relation#fromをUNIONを含むリレーション(サブクエリ)と組み合わせて使っているコード全般。- 特に「第2引数でエイリアスを指定しているが、今までは動かなかった/偶然動いていた」ようなケースで挙動が変わる可能性があります。
- 期待される変化:
- これまでエイリアスが無視されていたケースで、正しく
AS aliasが付くようになります。 - そのため、
SELECTなどでそのエイリアス名を前提としている複雑なクエリが正しく動作するようになります。
- これまでエイリアスが無視されていたケースで、正しく
- 注意点:
- すでにこの不具合を回避するために、生 SQL や
Arel.sqlを使って無理やりエイリアスを付けていた場合、二重にエイリアスが付いてしまう等の影響が出る可能性があります。- 例: すでに
"(#{union.to_sql}) some_models"のような文字列を渡している場合など。
- 例: すでに
- DB ごとにサブクエリ + UNION + エイリアスのサポート状況・制約が異なる可能性はありますが、一般的な RDBMS(PostgreSQL, MySQL 等)では問題なく動作する構成です。
- すでにこの不具合を回避するために、生 SQL や
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/54829
- 関連する API:
ActiveRecord::QueryMethods#fromActiveRecord::Relation#union
- 派生的な利用パターンの例:
union = User.select(:id, :name).where(active: true)
.union(User.select(:id, :name).where(guest: true))
# UNION 結果を from に乗せ替えつつエイリアス user_union として扱う
User.from(union, :user_union)
.where("user_union.name LIKE ?", "%foo%")このような「複雑な条件を UNION でまとめてから ActiveRecord のクエリチェーンを続ける」パターンが、より安全かつ自然に書けるようになります。
#57227 Fix duplicate entries accumulating in aliases_by_attribute_name
マージ日: 2026/4/25 | 作成者: @njakobsen
- 概要 (1-2文で)
alias_attributeが同じエイリアスを何度も登録した場合に内部配列に重複が蓄積し、その結果define_attribute_methodsが同じメソッドを何度も再生成してしまう問題を修正する PR です。Rails 7.1 以降でtable_nameを繰り返し変更するようなパターンで、クエリ時間やオブジェクト生成数がどんどん増大していた回帰バグを解消します。
- 変更内容の詳細
問題の背景
- 対象メソッド:
ActiveModel::AttributeMethods#alias_attribute - Rails 7.1 からの振る舞い:
alias_attributeはaliases_by_attribute_name[old_name]にnew_nameを単純に<<していた。- 重複チェックがないため、同じクラスで同じ
alias_attributeを複数回呼ぶと、内部配列に同じ文字列が何度も入り続ける。
- 同時に、Rails 7.1 では以下の変更が入っている:
ActiveRecord::Base#load_schema!が、idカラムごとに毎回alias_attribute :id_value, :idを自動で呼ぶようになった。aliases_by_attribute_nameが永続的なストレージ構造として導入され、クラスに紐づいて蓄積されるようになった。
結果として、
- 再利用可能な AR クラスに対して
self.table_name = ...を繰り返し変更すると、その都度load_schema!→alias_attribute :id_value, :idが走る。 - そのたびに
aliases_by_attribute_name["id"]に"id_value"が追記され、["id_value"]→["id_value", "id_value"]→["id_value", "id_value", "id_value"]… と増えていく。 define_attribute_methodsはaliases_by_attribute_nameをもとに alias メソッドを生成するため、"id_value"が N 個あれば N 回同じパターンの処理が走る。- ベンチマーク例では、約 1000 回の
table_name=切り替えで、1 クエリあたりのコストが ~12ms → ~90ms、オブジェクト割り当てが ~41k → ~246k 個まで増加していた。
Rails 7.0 以前は:
alias_attributeがその場でメソッドを定義しており(attribute_method_matchers.each内で)、aliases_by_attribute_name自体が存在しなかった。id_valueの自動 alias もなかったため、同じパターンでも実質的に問題は顕在化していなかった。
修正内容
alias_attribute 内で aliases_by_attribute_name に値を追加する際に、重複を避けるように変更されています。
元のロジック(イメージ):
aliases_by_attribute_name[old_name] << new_name修正後:
aliases = aliases_by_attribute_name[old_name]
aliases << new_name unless aliases.include?(new_name)これにより、同じ (old_name, new_name) の組み合わせで alias_attribute を何度呼んでも、aliases_by_attribute_name[old_name] の中身は一意な new_name のみが残り、配列が肥大化しません。
その他の点:
attribute_aliases.merge(new_name => old_name)はもともと idempotent(同じ値で再度 merge しても結果が変わらない)なのでそのまま。eagerly_generate_alias_attribute_methodsは複数回呼ばれても安全:- 内部で呼ばれる
define_attribute_method_patternが、instance_method_already_implemented?を見て早期 return するため、既存メソッドを再定義しない。
- 内部で呼ばれる
追加されたテスト
ActiveModel 単体テスト
- 場所:
activemodel/test/cases/attribute_methods_test.rb - 内容:
- あるクラスで
alias_attribute :bar, :fooを3回呼ぶ。 aliases_by_attribute_name["foo"]が["bar"]だけであることを検証。
- あるクラスで
- 未パッチ時:
- 実際の値は
["bar", "bar", "bar"]となりテスト失敗。
- 実際の値は
- パッチ適用後:
["bar"]となりテスト成功。
- 場所:
ActiveRecord 統合テスト
- 場所:
activerecord/test/cases/attribute_methods_test.rb - 内容:
- 匿名の
ActiveRecord::Baseサブクラスを作成。 table_nameをtopicsとpostsの間で切り替えながら.firstを実行する、という実際の再現パターンに近い流れをテスト。- 3回クエリ実行後の
aliases_by_attribute_name["id"]が["id_value"]だけであることを検証。
- 匿名の
- 未パッチ時:
["id_value", "id_value"]など重複が残りテスト失敗。
- パッチ適用後:
["id_value"]となり成功。
- 場所:
全体として:
activemodelフルスイート (1168 tests)activerecordsqlite3 フルスイート (9418 tests)
において、新たな失敗は発生していないことが確認されています。
- 影響範囲・注意点
影響を受ける主なケース:
- Rails 7.1 以降を利用している。
- 同じモデルクラスに対して:
alias_attributeを同じ引数で複数回呼んでいる、またはtable_nameを動的に切り替えるクラス(マルチテナント実装やシャーディングなど)で、内部的にload_schema!→alias_attribute :id_value, :idが何度も呼ばれている。
この PR による挙動変化:
aliases_by_attribute_name[old_name]に格納されるnew_nameの配列が「重複なし」になる。- すでに定義済みの alias メソッドの有無にかかわらず、API 上の表面挙動 (
alias_attribute呼び出しの結果としてアクセスできるメソッド) は変わらない。 - 変わるのは内部状態(配列の中身)と、
define_attribute_methods実行時のパフォーマンスのみ。
後方互換性・リスク:
- 既存アプリ側が
aliases_by_attribute_nameの中の重複を前提にしている可能性はほぼなく、通常は内部実装依存なので、互換性リスクは極小。 - 外から見えるメソッド定義結果も変わらないため、挙動の意味的な変化はありません(バグ修正のみ)。
- CHANGELOG は未更新で、「軽微なバグ修正」として扱われています。
この修正により期待できる効果:
- 長時間動作するプロセスで
table_nameを動的に変更するようなコードの性能劣化・メモリ断続増加が抑制される。 - モデルが多く、
alias_attributeを多用しているアプリでも、同じ alias を再設定してしまった場合のオーバーヘッドがなくなる。
- 参考情報 (あれば)
- 回帰の原因となった変更:
id_value自動エイリアス追加:
https://github.com/rails/rails/commit/e90b11e77ealiases_by_attribute_name導入:
https://github.com/rails/rails/commit/0f5563bd40
alias_attribute実装比較:
この PR (#57227) 自体は、aliases_by_attribute_name の重複排除という一点に絞った小さい変更で、性能回復と内部状態の健全化を目的としたバグフィックスです。
#57245 Fix find_signed for models with a composite primary key
マージ日: 2026/4/25 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::SignedId#find_signedが複合主キー(CPK)を持つモデルでArgumentErrorを投げていた不具合を修正し、find_signed!および通常のfindと同等に動作するようにした PR です。これにより、サインインリンクやパスワードリセットなどで CPK モデルの signed_id を安全に利用できます。
- 変更内容の詳細
不具合の内容
対象: ActiveRecord::SignedId::ClassMethods#find_signed
前提となるモデル:
class Cpk::Order < ActiveRecord::Base
self.primary_key = [:shop_id, :id]
end
order = Cpk::Order.first
token = order.signed_id挙動:
Cpk::Order.find_signed!(token)
# => 正常にレコードを返す
Cpk::Order.find_signed(token)
# => ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array原因は find_signed 内部での検索条件の組み立て方です。
旧実装(問題箇所のイメージ)
if id = signed_id_verifier.verified(...)
find_by(primary_key => id)
end- CPK の場合:
primary_keyは["shop_id", "id"]idは[37647470, 75294940]のような配列
find_by(["shop_id", "id"] => [37647470, 75294940])の形になり、PredicateBuilder は- 「キーが配列 ⇒ 複数レコード指定」と解釈
- 値も「配列の配列」を期待する(
[[shop_id, id], [shop_id, id], ...]) - 実際は一次元配列なので
ArgumentErrorを投げる
一方 find_signed! は find(id) を使い、finder_methods.rb 内で
primary_key.zip(id).to_h
# => { shop_id: 37647470, id: 75294940 }の形に変換してから検索するため問題が出ていませんでした。
今回の修正内容
find_signed 側でも CPK を意識した条件生成を行うよう変更されています(実際のコードは 1 行差分ですが、意図としては以下のようなロジック):
if id = signed_id_verifier.verified(...)
if composite_primary_key?
# CPK の場合: 各主キー列と値をペアにする
find_by(primary_key.zip(id).to_h)
# => { shop_id: 37647470, id: 75294940 } のような Hash になり、通常の where と同じ扱い
else
# 単一主キーの場合は従来どおり
find_by(primary_key => id)
end
endポイント:
- CPK 時に
primary_key(配列)とid(配列)をzipして Hash に変換することで、- PredicateBuilder から見て「普通の Hash 条件」になる
where(shop_id: ..., id: ...)と同等の条件で検索される
findではなくfind_byを使い続けている理由:RelationMethods#find_signedでスコープ付きに使えることを維持するため
(例:Cpk::Order.where(active: true).find_signed(token))
token_for との整合性
- 過去に
find_by_token_forに同種のバグがあり、primary_key => idをprimary_key => [id]に修正済み - 今回は同じ「CPK + PredicateBuilder」の問題を、より明示的な
zipによる Hash 生成で解決している- どちらのアプローチも最終的な SQL は同じ
zipの方が「各キーと値を 1:1 に対応させている」と読み取りやすく、find_oneの既存実装とも揃う
テスト
signed_id_test.rb に以下のテストが追加されています:
- CPK モデルでの
find_signedラウンドトリップrecord.signed_id→find_signed(token)で元のレコードが取得できること
- CPK + Relation での
find_signed- スコープに合致する場合はレコードを返す
- スコープに合致しない場合は
nilを返す
- CPK での
find_signed!ラウンドトリップ(リグレッション防止)
- 影響範囲・注意点
影響を受けるケース:
self.primary_key = [:col1, :col2, ...]のように複合主キーを使っているモデルでsigned_id/find_signedを利用しているアプリ- 例: マジックリンクサインイン、パスワードリセット、メールアドレス確認など
以前との挙動差:
- これまでは
find_signedがArgumentErrorで落ちていたパスが、正しくレコードを返すようになる find_signed!の挙動・例外発生条件は従来どおり(該当レコードがなければActiveRecord::RecordNotFound)
- これまでは
互換性:
- 単一主キーのモデルに対する挙動は変更なし
- CPK モデルでもクエリの条件自体は「主キーの各カラムを等価条件で検索」という素直な形なので、意図しない結果に変わる可能性は低い
- 既に
find_signedを rescue するワークアラウンド(ArgumentErrorを拾うなど)を入れていた場合、その処理は不要になるか、挙動が変わる可能性がある
パフォーマンス:
- 追加される処理は
primary_key.zip(id).to_hのみで、レコード 1 件の検索に対してごく軽微 - 実行される SQL は従来
find_signed!が発行していたものと同等
- 追加される処理は
- 参考情報 (あれば)
- 類似バグ修正:
find_by_token_forの CPK 対応
https://github.com/rails/rails/commit/8617a7ce2ef4236f085e967b3fe3159e82bdb5d5 - CPK +
findの実装パターン(find_one内)
https://github.com/rails/rails/blob/ff34428f313091cd579d7790b13cd7206c2efab9/activerecord/lib/active_record/relation/finder_methods.rb#L529-L533 - 問題の原因となった
find_signedの元コード
https://github.com/rails/rails/blob/ff34428f313091cd579d7790b13cd7206c2efab9/activerecord/lib/active_record/signed_id.rb#L73
#57220 Guides: remove outdated link to Action Cable examples
マージ日: 2026/4/22 | 作成者: @tjschuck
- 概要 (1-2文で)
Railsガイド「Action Cable Overview」から、9年間更新されておらずアーカイブされた外部リポジトリ(actioncable-examples)へのリンクが削除されました。これにより、公式ガイドから古い・メンテされていない情報源への誘導がなくなります。
- 変更内容の詳細
対象ファイル:
guides/source/action_cable_overview.md
変更内容:
- 「More complete examples(より完全なサンプル)」として案内されていた GitHub リポジトリ
https://github.com/rails/actioncable-examplesへのリンクが削除されました。 - 実質的には、Action Cable ガイド内の「より大きな例/詳細なサンプルコードはこちら」といった説明行が5行分なくなっただけで、コードやAPIの説明文自体には手が入っていません。
イメージとしては、ガイド中の以下のような記述が削除された形です(擬似例):
## More complete examples
You can find more complete examples in the following repository:
https://github.com/rails/actioncable-examples実際の差分は +0 / -5 行であり、新たなリンク先や代替コンテンツの追加は行われていません。
- 影響範囲・注意点
影響範囲
- 公式ガイド(Action Cable Overview)を読んだ際に、外部のサンプル集への導線がなくなります。
- Rails自体のコード、Action Cable のAPI、挙動には一切変更がありません。アプリケーションコードへの影響はゼロです。
- すでに
rails/actioncable-examplesをブックマークしている人や、過去のガイドからリンクを踏めていた人には変化はありません(リポジトリ自体は存在し、アーカイブ済みという状態)。
注意点 / 開発者が意識すべきこと
- 公式ガイドから「推奨されるサンプル実装」としてはもはや扱われていない、というメッセージと受け取れます。
actioncable-examplesは Rails の古いバージョンを前提にしている可能性が高く、現行バージョンのベストプラクティスとは乖離していると考えた方が安全です。- Action Cable のサンプルやベストプラクティスを探す場合は:
- 現行の Rails ガイド(Action Cable Overview, Getting Started with Rails + Action Cable の章など)
- Rails の公式リポジトリ内のテスト・サンプル
- 信頼できるブログ・書籍・チュートリアル(Rails バージョンを明示しているもの) を優先するのがよいです。
- 参考情報 (あれば)
該当ガイド(現行版)
- https://guides.rubyonrails.org/action_cable_overview.html#more-complete-examples
※ この PR マージ後は、該当セクションの内容が変わっている可能性があります。
- https://guides.rubyonrails.org/action_cable_overview.html#more-complete-examples
アーカイブされたサンプルリポジトリ
- https://github.com/rails/actioncable-examples
- Archived / 9年間更新なし
- 古いRailsバージョンのAction Cableの参考にはなりますが、現行バージョンへの適用には注意が必要です。
- https://github.com/rails/actioncable-examples
PR本体
#57212 [ci skip] Fix a typo in the ActionMailbox basics docs
マージ日: 2026/4/22 | 作成者: @mgriffin
概要 (1-2文で)
Action Mailbox の「Basics」ガイド内にあった英文のタイポ(and→an)を修正するドキュメント専用のPRです。コードや挙動の変更は一切なく、テストガイドの文章を自然な英語に整えています。変更内容の詳細
- 対象ファイル:
guides/source/action_mailbox_basics.md - 変更内容は1行のみで、テストヘルパーを使った受信メールテストの説明文から、文法上の誤りを修正しています。
修正前(イメージ):
Here is and example of testing an inbound email with Action Mailbox TestHelpers修正後:
Here is an example of testing an inbound email with Action Mailbox TestHelpers意味としては「Action Mailbox の TestHelpers を使って受信メールをテストする例は次のとおりです」という文で、and というタイプミスを正しい冠詞 an に直しただけです。
- 影響範囲・注意点
- ランタイム動作への影響なし
- アプリケーションコード、Action Mailbox の実装、テストコードには一切変更がありません。
- バージョンアップに伴う互換性問題やマイグレーションは不要です。
- 開発者ドキュメントの読みやすさ向上のみ
- 英語ドキュメントが少し読みやすくなった、というレベルの変更です。
- 既存のガイドの手順・サンプルコードの意味は変わりません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57212
- 関連ドキュメント:
Action Mailbox Basicsガイド- Action Mailbox のテストに関するセクションで、
ActionMailbox::TestHelperなどを使った受信メールテストの書き方を説明している部分の文言修正です。
- Action Mailbox のテストに関するセクションで、
#55282 Bump required PostgreSQL version to 10.0
マージ日: 2026/4/22 | 作成者: @yahonda
- 概要 (1-2文で)
Rails が公式にサポートする PostgreSQL の最小バージョンを「9.3 以上」から「10 以上」に引き上げた PR です。これにより、今後リリースされる PostgreSQL 18 以降や pg gem 1.6.x との互換性を確保します。
- 変更内容の詳細
なぜ「10.0 以上」が必要になったか
- PostgreSQL 18 で「キャンセルリクエストキー」のフォーマットが変更され、Rails のテスト(
ActiveRecord::PostgresqlTransactionTest#test_raises_Interrupt_when_canceling_statement_via_interrupt)が失敗する問題が発生。 - この問題自体は pg gem 側で修正済み(
pg 1.6.0に含まれる)。 - しかし、pg 1.6 が PostgreSQL 10 以上を必須とする変更を入れたため(https://github.com/ged/ruby-pg/pull/606)、Rails が「PostgreSQL 9.3+」を名乗り続けると、Rails・pg・PostgreSQL 18 の組み合わせで整合性が取れなくなる。
- そこで Rails 側もサポート表記とコードを更新し、最低要件を PostgreSQL 10 に引き上げた、という経緯です。
主なコード/設定の変更ポイント
ActiveRecord の PostgreSQL アダプタ周り
postgresql_adapter.rb/schema_statements.rbなどで、- 9.3〜9.6 など「古い PostgreSQL バージョンを考慮した分岐・ワークアラウンド」を削除・簡略化。
- 「10 以降で常に有効」な仕様を前提にした実装に整理。
- 例:
- 旧バージョンを考慮していたバージョンチェック(
postgresql_version < 100000のような条件分岐)が消え、単純化されている。 - 一部の型・機能(UUID, enum, geometric 型など)に対するテストやスキーマ定義から、古いバージョン専用パスが削除。
- 旧バージョンを考慮していたバージョンチェック(
テストコードの整理
activerecord/test/cases/adapters/postgresql/*.rbの複数ファイルで、- 「9.x 以下ではスキップ」「9.3 だとこう振る舞う」等の条件付きテストを削除。
- PostgreSQL 10 以降を前提とした期待値に統一。
uuid_test.rbなどで行数の増減が大きいのは、古いバージョン向けの分岐削除と、それに応じたテストケースの再編が主な理由です。
スキーマ定義
activerecord/test/schema/postgresql_specific_schema.rbでも、古いバージョンに合わせたスキーマ定義や条件分岐を削除し、10 以降で前提となる仕様に合わせて整理。
ドキュメントとテンプレート
guides/source/active_record_postgresql.mdguides/source/command_line.mdrailties/.../config/databases/postgresql.yml.ttなどで、サポートバージョン表記やサンプル設定を「PostgreSQL 10 以上」を前提とした内容に更新。- 新しく Rails アプリを
rails newしたときに生成されるconfig/database.ymlの PostgreSQL テンプレートも、PostgreSQL 10 を前提にした内容になります。
CHANGELOG の更新
activerecord/CHANGELOG.mdに- 「Active Record がサポートする PostgreSQL の最小バージョンを 10 に引き上げた」
- という旨のエントリが追加されています。
→ バージョンアップ時の破壊的変更(breaking change)として明示。
影響範囲・注意点
アプリケーション側の必須要件の変更
- Rails(main / 次期メジャー)+ PostgreSQL アダプタを利用する場合、本番/開発環境とも PostgreSQL 10 以上が必須になります。
- 9.x 系(9.6, 9.5, 9.4, 9.3 など)を使い続けている場合:
- このバージョンの Rails にはアップグレードできません。
- もしくは PostgreSQL を 10 以上に上げる必要があります。
pg gem のバージョンとの整合性
- pg 1.6 以降を使う場合:
- pg 自体が PostgreSQL 10 以上を要求するため、Rails 側のこの変更と整合します。
- PostgreSQL 18(またはそれ以降)を使う予定なら、**Rails + pg 1.6+ + PostgreSQL 10+**の組み合わせが前提になります。
- 逆に「PostgreSQL 9.x + Rails main + 最新 pg」を混ぜる構成は、サポート外かつ実質的に動作しません。
- pg 1.6 以降を使う場合:
テスト/開発環境の CI 設定
- CI(GitHub Actions, CircleCI など)で PostgreSQL 9.x を使っている場合は、ジョブ定義を更新して 10 以上を使うように変更する必要があります。
- テストコード側から「古いバージョン向けのワークアラウンド」が消えているため、CI を更新しないとテストがそもそも通らない or 動かない可能性があります。
古い PostgreSQL 向けの後方互換性の喪失
- これまで非公式に「たまたま動いていた」ような 9.x 環境での挙動は、この PR 以降は保証されません。
- 古いサーバへの接続、マイグレーション、特定の型(UUID, enum, geometric 型など)に絡む挙動についても、9.x で問題が出たとしてもバグとはみなされない前提になります。
- 参考情報 (あれば)
- この PR の元になった議論
- Proposal: Bump the minimum supported PostgreSQL version from 9.3 to 10
https://discuss.rubyonrails.org/t/proposal-bump-the-minimum-supported-postgresql-version-from-9-3-to-10/89422
- Proposal: Bump the minimum supported PostgreSQL version from 9.3 to 10
- PostgreSQL の cancel request key 変更コミット
- pg gem 側の対応 PR
- PostgreSQL 18 のキャンセルキー対応: https://github.com/ged/ruby-pg/pull/614
- 最低サポート PostgreSQL バージョンを 10 に引き上げ: https://github.com/ged/ruby-pg/pull/606
この PR を前提に Rails を使う場合は、「PostgreSQL 本体のバージョンポリシーを 10+ に揃える」「pg gem を 1.6+ に上げる」ことをセットで検討しておくとスムーズです。