ActiveRecord 7.2(Rails 7.2.0、2024年8月9日公開)では、モデルの外からコミット後の処理を登録するAPI、inspectに出す属性の指定、データベース接続の借り方の3点が大きく変わりました。この記事では、同じスクリプトをactiverecord 7.1.6・7.2.4・8.1.3.1で実行した結果をもとに、各変更の挙動と、アップグレード時に壊れやすい箇所を説明します。Railsフレームワーク全体の変更点やサポート期限はRails 7.2の主な変更点とサポート終了日で扱っています。
まとめ
- トランザクション:
transactionのブロック引数がActiveRecord::Transactionになり、after_commitとafter_rollbackをモデルの外から登録できます。ActiveRecord.after_all_transactions_commitは、トランザクションの外で呼ぶと即時に実行され、中で呼ぶと最も外側のコミット後に実行されます。 - attributes_for_inspect:7.2.0では既定値が
[:id]でしたが、7.2.2で:allに戻りました。現在は新規アプリのproduction.rbが[:id]を設定します。rails consoleでは基底クラスの設定が:allになりますが、モデル単位の上書きは有効です。 - 接続:
ActiveRecord::Base.connectionはlease_connectionへ改名されました(警告の出ない非推奨)。短時間だけ接続を借りるならwith_connectionを使います。 - クエリ:
pluckとorderがテーブル名をキーにしたハッシュを受け付けるようになりました。7.1.6では同じコードが例外になります。 - 移行の注意:
serialize :col, JSONのように第2引数でcoderを渡す書き方は7.2で削除され、ArgumentErrorになります。 - 今回のスクリプトで確認したAPIは8.1.3.1でも同じ挙動でした。8系への移行では、アプリケーション固有の設定や依存gemも別途検証してください。7.2系自体は公式のサポート対象一覧から外れています。
以下、7.1.6との差分が大きい順に見ていきます。
ActiveRecord 7.2で追加・変更された機能と7.1・8.1での挙動
検証に使ったのはRuby 4.0.6とSQLiteで、activerecordだけを版ごとに分けたgemディレクトリに入れています。7.1.6はsqlite3 gem 1.7.3、7.2.4と8.1.3.1は2.9系を使いました。
| 機能 | 7.1.6 | 7.2.4 | 8.1.3.1 |
|---|---|---|---|
transactionのブロック引数 |
nil |
ActiveRecord::Transaction |
同左 |
current_transaction |
未定義 | あり | 同左 |
after_all_transactions_commit |
未定義 | あり | 同左 |
attributes_for_inspect |
未定義 | あり(既定:all) |
同左 |
with_connection / lease_connection |
未定義 | あり | 同左 |
pluck(posts: [:id]) |
UnknownAttributeReference |
配列を返す | 同左 |
order(comments: { id: :desc }) |
ArgumentError |
SQLを生成 | 同左 |
in_batches.destroy_allの戻り値 |
nil |
削除件数 | 同左 |
8.1.3.1では、版番号・生成日時・オブジェクトIDを除き、検証スクリプトで確認したAPIの出力が7.2.4と一致しました。ただし8.0は実行しておらず、アプリケーション全体の互換性を確かめた結果でもありません。
トランザクションコールバックの新API:after_commit・current_transaction・after_all_transactions_commit
7.1までは、コミット後に通知やジョブを送るには、モデルにafter_commitを書くしかありませんでした。サービスオブジェクトのようにモデルの外でトランザクションを張るコードでは、処理をどこに書くかが問題でした。7.2では3つの入口が追加されています。
transactionのブロック引数に渡るActiveRecord::Transaction
Post.transaction do |tx|
tx.after_commit { NotifyJob.perform_later } # コミット後に1回
tx.after_rollback { Rails.logger.warn("rolled back") }
Post.create!(title: "t")
end
# 7.2.4 の実行結果: yield class=ActiveRecord::Transaction
# コミット時は after_commit だけ、raise ActiveRecord::Rollback 時は after_rollback だけが動く
7.1.6で同じブロックを実行すると、引数はnilです。tx.after_commitはNoMethodErrorになるため、このAPIを使うコードは7.2未満では動きません。
ネストしたトランザクションで登録したコールバックの発火タイミング
内側のブロックで登録したコールバックも、内側のブロックを抜けた時点では動かず、外側がコミットされた時点で動きます。requires_new: trueでセーブポイントを切った場合も同じでした。
log = []
Post.transaction do
Post.transaction(requires_new: true) do |inner|
inner.after_commit { log << "savepoint commit cb" }
end
log << "after savepoint released"
end
log # => ["after savepoint released", "savepoint commit cb"]
セーブポイントの解放はデータベースへの確定ではありません。内側のブロックを抜けた時点でジョブを投入する設計にすると、外側がロールバックしたときに存在しないレコードを処理するジョブが残ります。7.2のコールバックはこの順序を守っているので、手動で順序を管理する必要はありません。
after_all_transactions_commitとcurrent_transactionの使い分け
ActiveRecord.after_all_transactions_commitは、トランザクションの中で呼ばれるか外で呼ばれるかがわからないメソッドのために用意されています。CHANGELOGの説明は「Useful for code that may run either inside or outside a transaction」です。
log = []
ActiveRecord.after_all_transactions_commit { log << "outside->immediate" }
Post.transaction do
Post.transaction(requires_new: true) do
ActiveRecord.after_all_transactions_commit { log << "inner->after outermost" }
end
log << "outer body end"
end
log # => ["outside->immediate", "outer body end", "inner->after outermost"]
挙動を確かめた結果は次のとおりです。
- トランザクションの外で呼ぶと、ブロックはその場で実行されます。
- 外側が
ActiveRecord::Rollbackで巻き戻ると、ブロックは実行されません。 - 内側のセーブポイントだけが巻き戻り、外側がコミットされた場合も実行されません。
Post.current_transactionは、トランザクションの外で呼んでもnilを返しません。open?がfalseのActiveRecord::Transactionを返し、それに登録したafter_commitは即時に実行されました。使い分けの基準は、特定の接続のトランザクションを対象にするか、登録時に開いている複数接続のトランザクションすべてを対象にするかです。データベースが1つのアプリでは、検証の範囲ではどちらも同じ順序で動きました。複数データベース構成で、特定のデータベースのコミットだけを待つならPost.current_transaction、関係するすべてのコミットを待つならafter_all_transactions_commitを選びます。
Active Jobのperform_laterをトランザクション内で呼んだときにコミット後まで遅らせるenqueue_after_transaction_commitも7.2で入りました。ただし設定値の書き方が8.0・8.1で変わっているため、詳細はRails 7.2の主な変更点とサポート終了日とRails Active Jobの解説記事にまとめています。
attributes_for_inspect:7.2.0と7.2.2で変わったinspectの既定値
「7.2にしたらinspectがidしか出さなくなった」という報告は、7.2.0と7.2.1に特有の現象です。既定値は7.2の途中で変わっているので、使っているパッチ版を先に確認してください。
7.2.0〜7.2.1と7.2.2以降の既定値の違い
| 版 | クラス属性の既定 | 開発・テスト | 本番 | rails console |
|---|---|---|---|---|
| 7.2.0〜7.2.1 | [:id] |
:all(railtieが設定) |
[:id] |
環境の設定に従う |
| 7.2.2〜8.1 | :all |
:all |
新規アプリのproduction.rbで[:id] |
既定:all(モデル設定優先) |
7.2.0のcore.rbはclass_attribute :attributes_for_inspect, instance_accessor: false, default: [:id]で、開発環境だけrailtieのconsider_all_requests_local判定で:allに切り替えていました。7.2.2(2024年10月31日公開)のactiverecord CHANGELOGには「Set .attributes_for_inspect to :all by default.」「In the console all the attributes are always shown.」とあり、railtieのconsoleブロックにActiveRecord::Base.attributes_for_inspect = :allが追加されています。
今の構成でidしか出ないのは、本番のログやエラー通知に記録されるときです。新規アプリのconfig/environments/production.rbには次の1行が生成されます。7.2.0以前に作ったアプリにはこの行がないため、7.2.2以降へ上げると本番でも全属性が出るようになります。
# config/environments/production.rb(7.2.2以降の生成テンプレート)
# Only use :id for inspections in production.
config.active_record.attributes_for_inspect = [ :id ]
# モデル単位で上書きする場合
class Post < ApplicationRecord
self.attributes_for_inspect = [:id, :title]
end
Post.first.inspect # => #<Post id: 1, title: "Hello">
Post.first.full_inspect # 設定を無視して全属性を返す
本番でinspectを絞る理由:配列の文字列化で起きる処理時間の差
この機能を実装したShopifyのAndrew Novoselac氏は、Rails at Scaleの記事で、あるリクエストでinspectに約8秒かかっていた事例を紹介しています。原因は、数百件のモデルを含む配列にto_sを呼んでいたことでした。配列のto_sはinspectの別名なので、文字列への埋め込みのようにinspectを呼んでいるとは見えない書き方でも、要素ごとに全属性が展開されます。filter_attributesによる秘匿処理も属性ごとに走ります。
配列の文字列化による負荷を調べるため、33列・500件の合成データを用意し、filter_attributesに6項目を指定して測定しました。"#{users}"をウォームアップ後に5回実行した最短時間は次のとおりです。測定時間は実行環境によって変わります。
| attributes_for_inspect | 処理時間 | 生成される文字列 |
|---|---|---|
:all |
111.2ms | 350,512字 |
[:id] |
3.4ms | 8,392字 |
例外メッセージやログでモデルが展開される経路は、意外と多くあります。本番は[:id]のままにし、調査ではfull_inspectを明示して呼ぶのが安全です。7.2.2より前のアプリを上げる場合は、production.rbにこの1行を追加し忘れないでください。
with_connectionとlease_connection:データベース接続の借り方の変更
7.1までのActiveRecord::Base.connectionは、接続をプールから取り出すと、リクエストやジョブが終わるまで保持し続けます。スレッドやFiberの数が接続数を上回る構成では、これがプールの枯渇につながっていました。7.2ではこのメソッドがlease_connectionへ改名され、短時間だけ接続を借りるwith_connectionが追加されています。
# トランザクション外で、既存の接続を保持していない状態で実行する
ActiveRecord::Base.connection_handler.clear_active_connections!
pool = ActiveRecord::Base.connection_pool
ActiveRecord::Base.with_connection { |c| c.select_value("SELECT 1") }
pool.active_connection? # => nil(ブロックを抜けると返却される)
Post.count
pool.active_connection? # => nil(7.2のモデルのクエリも保持しない)
ActiveRecord::Base.lease_connection
pool.active_connection? # => SQLite3Adapter(リクエスト終了まで保持)
CHANGELOGには、.connectionの非推奨は「soft deprecation」で警告を出さず、削除の予定もないと書かれています。既存コードを洗い出したい場合は、config.active_record.permanent_connection_checkoutを使います。7.2.4で切り替えた結果は次のとおりでした。
true(既定):何も出ません。:deprecated:Called deprecated `ActiveRecord::Base.connection` method.の警告が出て、処理は続きます。:disallowed:同じ文言でActiveRecord::ActiveRecordErrorが発生します。
まずテスト環境で:deprecatedにして呼び出し箇所を一覧にし、自前のコードをwith_connectionへ置き換えてから:disallowedにする順番が無難です。gemが内部で.connectionを呼んでいる場合、:disallowedにすると本番でエラーになります。依存gemを含む呼び出し箇所の検証が済むまでは、本番で:disallowedを有効にしないでください。
pluckとorderのハッシュ指定:結合先テーブルの列指定と並べ替え
joinsした先の列を取得・並べ替えるとき、7.1までは"comments.body"のような文字列やArel属性で指定していました。7.2ではwhereと同じ形のハッシュで書けます。
Post.joins(:comments).order("comments.id")
.pluck(posts: [:id], comments: [:id, :body])
# => [[1, 1, "c1"], [1, 2, "c2"], [2, 3, "c3"]]
Post.joins(:comments).order(comments: { created_at: :desc }, title: :asc).to_sql
# => ... ORDER BY "comments"."created_at" DESC, "posts"."title" ASC
7.1.6で同じコードを実行すると、pluckはActiveRecord::UnknownAttributeReference(Dangerous query method)、orderはArgumentError: Direction "{created_at: :desc}" is invalid.になります。includesと組み合わせたorder(comments: { created_at: :desc })は、LEFT OUTER JOINでeager loadするSQLになりました。ハッシュ指定ではテーブル名と列名がクオートされますが、列名のタイプミスをSQL実行前に検出する機能ではありません。
バッチ処理・関連付け・PostgreSQLまわりの細かな変更
in_batches.destroy_allは削除件数(Integer)を返すようになりました。7.1.6では常にnilです。削除件数をログに残す処理を自前で数えていた場合は置き換えられます。in_batches.touch_allが使えるようになりました。counter_cache: { active: false }で、値の埋め込みが終わるまでカウンタ列を読まずに済むようになりました。手順はcounter_cacheの実装手順と後付けの対処で扱っています。strict_loading!(mode: :n_plus_one_only)は子の関連を先読みしなくなり、person.posts.first.firmが例外にならなくなりました。N+1の検出方法はN+1問題の実測と検出方法を参照してください。- PostgreSQLアダプタは、
select_valueなどで取得したdate型の値を文字列でなくDateで返します。生SQLの結果を文字列として比較していたコードは、7.2で比較結果が変わります。
7.1から7.2へ上げるときにActiveRecord側で壊れやすい箇所
アップグレード手順の全体はRails 7.2の移行判断の記事に任せます。ここでは、ActiveRecordの範囲で実際にエラーや挙動の変化が起きる箇所を挙げます。
- serializeの第2引数:
serialize :body, JSONは7.2.4でArgumentError: wrong number of arguments (given 2, expected 1)になります。serialize :body, coder: JSONに書き換えてください。7.1までは非推奨警告だけで動いていたため、起動するまで気づきにくい箇所です。 - 本番のinspect:7.2.0か7.2.1へ上げると本番のログやエラー通知からid以外の属性が消え、7.2.2以降へ上げると今度は全属性が出ます。エラー通知サービスに個人情報が流れていないかを、上げた直後に確認してください。
- マイグレーションの版指定:7.2.4の
Migration::Compatibility::V7_1は中身のないclass V7_1 < V7_2です。既存のMigration[7.1]を[7.2]へ書き換えても生成されるDDLは変わらないので、一括で置換する必要はありません。 - gemの対応版:
Base.connectionはそのまま動くため互換性の問題は出にくいものの、permanent_connection_checkout = :disallowedを本番に入れるとgem側の呼び出しで落ちます。
7.2系は、2026年10月時点で公式のメンテナンスページの「List of currently supported releases」に載っておらず、載っているのは8.1.xと8.0.xだけです。2026年9月24日に7.2.4(PostgreSQLアダプタの再接続処理の修正など)が出ましたが、公式のリリース告知は「this is the last release of the 7.2.x series」と明記しています。今回のスクリプトで確認したAPIの挙動は8.1.3.1でも一致しましたが、移行時にはアプリケーション全体の回帰テストが必要です。
よくある質問
config.active_record.attributes_for_inspectを本番でも全属性にするには?
config/environments/production.rbのconfig.active_record.attributes_for_inspect = [ :id ]を削除するか、:allに書き換えます。7.2.2以降のrails consoleでは基底クラスの設定が:allになりますが、モデル単位の上書きは有効です。一時的に全属性を確認したいだけなら、設定は変えずにrecord.full_inspectを呼んでください。
after_commitとafter_all_transactions_commitの違いは?
モデルのafter_commitは、そのレコードを保存したトランザクションのコミットに紐づきます。ActiveRecord.after_all_transactions_commitは特定のレコードに紐づかず、開いているトランザクションがすべてコミットされた後に実行されます。トランザクションの外で呼んだ場合は即時に実行され、ロールバックされた場合は実行されません。
ActiveRecord::Base.connectionは使えなくなりますか?
使えます。7.2のCHANGELOGは、警告を出さない非推奨で削除の予定もないとしており、8.1.3.1でも既定の設定では警告なしで動きます。ただし接続をリクエストの終わりまで保持するので、新しく書くコードではwith_connectionを使ってください。
ActiveRecord 7.2の新機能はRails 8でも使えますか?
使えます。この記事で扱ったTransactionのコールバック、after_all_transactions_commit、attributes_for_inspect、with_connection、pluckとorderのハッシュ指定は、8.1.3.1でも同じ出力になりました。
Migration[7.2]と[7.1]で書き方は違いますか?
違いはありません。7.2.4の互換クラスV7_1は空のサブクラスで、[7.1]と[7.2]は同じ挙動になります。マイグレーションの基本的な書き方はActive Recordの基本を実例で解説した記事を参照してください。