Rails 7.0から7.1へのアップグレードの雑メモ
0. アップグレードの全体戦略
アップグレードでは、一度にすべての変更を適用するアプローチを避け、変更によるリスクを管理し、問題発生時の原因特定を容易にするため、作業を大きく2つのプルリクエスト(PR)に分割する段階的アプローチを採用します。
-
第一段階:基盤更新と互換性確保
- Railsのバージョンを7.1へ更新し、
rails app:update
コマンドによって新しい設定ファイルの骨格を導入します。 - APIの変更点を吸収し、既存コードとの互換性を確保するためのリファクタリングを中心に行います。
- これは、本格的な移行に向けた基盤整備と互換性対応のフェーズです。この段階で問題が発生した場合、原因はgemの互換性か、直接的なAPIの変更に起因する可能性が高いと判断できます。
- Railsのバージョンを7.1へ更新し、
-
第二段階:新デフォルト設定の完全適用
-
config.load_defaults 7.1
を設定し、Rails 7.1の新しいデフォルト設定を完全に有効化します。 - 新設定の適用によって初めて表面化する、挙動の変化や非推奨APIへの最終的なコード調整を行います。
- これは、アップグレードを完了させるための最終移行フェーズです。この段階での問題は、フレームワークの挙動に関する、より繊細な設定変更が原因であると推測できます。
-
この二段階アプローチにより、管理可能なスコープで変更を適用し、検証していくことが可能になります。これは、大規模かつ継続的に稼働するシステムを安全に進化させるための、極めて重要な戦略です。
1. 第一段階:互換性の確保と基盤整備
この段階の目標は、アプリケーションの基盤をRails 7.1に対応させ、コードを新しい環境の作法に合わせていくことです。
bundle update
)
1.1. 依存関係の更新 (全ての始まりは、プロジェクトの中核であるRails自体のバージョンを更新することから始まります。
- gem 'rails', '7.0.8.7'
+ gem 'rails', '~> 7.1', '< 7.2'
この '~> 7.1', '< 7.2'
というバージョン指定は、7.1系の最新のパッチバージョン(バグ修正など)は許容しつつ、後方互換性が失われる可能性のある7.2には自動で更新しない、という安全策です。
依存関係の更新には、--conservative
フラグを使用しました。
bundle update --conservative rails
このコマンドは、rails
gemから始まる依存関係のツリーを解析し、更新が必要なgemのみを対象とします。これにより、今回のアップグレードとは無関係なライブラリが意図せず更新される「カスケード効果」を防ぎ、変更範囲を最小限に抑えます。この手法は、アップグレードにおける定石です。更新後はGemfile.lock
の差分を精査し、どのライブラリのバージョンが変更されたかを正確に把握することが不可欠です。
rails app:update
)
1.2. 新しい設定への対応準備 (bundle update
の完了後、rails app:update
コマンドを実行します。これは単にファイルを生成するだけでなく、既存の設定と新しいフレームワークの推奨設定との差分(diff)を提示し、開発者が一つひとつの変更を受け入れるか、あるいは既存の設定を維持するかを選択できる対話的なツールです。
このステップで特に重要なのが、config/initializers/new_framework_defaults_7_1.rb
ファイルの生成です。このファイルは、Rails 7.1で導入された新しいデフォルト設定を、一つひとつ段階的に有効化するための移行支援ファイルです。
# Uncomment each configuration one by one to switch to the new default.
# Once your application is ready to run with all new defaults, you can remove
# this file and set the `config.load_defaults` to `7.1`.
# 例:
# Rails.application.config.active_record.run_after_transaction_callbacks_in_order_defined = true
# Rails.application.config.action_view.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor
全ての新設定を一度に適用するのではなく、例えばafter_commit
コールバックの実行順序の変更など、影響の大きい設定を個別に有効化してアプリケーションの動作を検証できるため、安全な移行が可能になります。
また、この段階でconfig.cache_classes
がconfig.enable_reloading
に改名されました。これは単なる名称変更に留まらず、「クラスをキャッシュするか」という実装の詳細から、「コードのリロードを有効にするか」という開発者の意図を直接反映した、より直感的で分かりやすい設定名への改善です。
attribute
APIへの移行:エラー駆動による改善
1.3. 今回のアップグレードにおいて、技術的に最も価値のある変更の一つが、モデル属性定義の刷新です。これは単なるリファクタリングではなく、Rails 7.1の厳格化によって発生したエラーに対応するための、必須の修正でした。
実際に、アプリケーションの複数の箇所で以下のようなエラーが観測されました。
RuntimeError
at/
Undeclared attribute type for enum 'gender' in Applikation. Enums must be backed by a database column or declared with an explicit type viaattribute
.
[GraphQL]:
Undeclared attribute type for enum 'fee_unit' in Contract. Enums must be backed by a database column or declared with an explicit type viaattribute
.
これらのエラーメッセージは、Applikation
モデルのgender
属性と、Contract
モデルのfee_unit
属性について、同じ問題を指摘しています。それは「enum
として定義された属性の型が宣言されていません。enum
はデータベースのカラムに対応しているか、attribute
メソッドで明示的に型を定義する必要があります」というものです。
エラーの根本原因
-
attr_accessor
の限界:attr_accessor
は、Active Recordの機能とは無関係な、純粋なRubyのメソッドです。そのため、attr_accessor
で定義された属性は、Active Recordの型システムやライフサイクル(バリデーション、コールバックなど)の管理下にありません。 -
enum
とActive Recordの密な関係: 一方でenum
は、Active Recordの強力な機能であり、内部的に型情報を必要とします。Rails 7.0までは、データベースに存在しない属性(仮想属性)に対してenum
を使用しても、ある程度寛容に動作していました。 -
Rails 7.1の厳格化: Rails 7.1では、
enum
の健全性を保つため、その対象となる属性がActive Recordに明確に認識されていることが必須となりました。その結果、attr_accessor
で定義された属性がenum
の要件を満たさなくなり、アプリケーションの通常リクエストとGraphQL APIの両方でエラーが発生したのです。
attribute
API
解決策としてのこの問題を解決する唯一の正しい方法が、attribute
APIへの移行です。
# 変更前
attr_accessor :gender
enum gender: { male: 1, female: 2, other: 3 } # エラーが発生
# 変更後
attribute :gender, :integer # 型を明示することでenumの要件を満たす
enum gender: { male: 1, female: 2, other: 3 } # 正常に動作
# 変更前
# (attr_accessor もしくは、それに類する定義が存在)
enum :fee_unit, UNITS # エラーが発生
# 変更後
attribute :fee_unit, :integer
enum :fee_unit, { percent: 1, jpy: 2 } # (値は例です)
この修正は、エラーを解消するだけでなく、アプリケーション全体の品質を向上させます。
-
データの整合性と信頼性の向上:
attribute :gender, :integer
と宣言することで、gender
属性が整数型であることをActive Recordに伝え、enum
が正しく機能するための前提条件を満たします。これにより、アプリケーションのデータ整合性が保証されます。 - 責務の分離とコードの簡潔化: 型変換のロジックをモデル自身が責務として持つことで、コントローラやビューのコードが簡潔になり、各コンポーネントが本来の役割に集中できます。
- 保守性と可読性の飛躍的向上: モデル定義を一読するだけで、そのモデルがどのようなデータ構造を持つべきかが明確に理解できます。これは将来のメンテナンスにおいて非常に価値があります。
このように、エラーをきっかけとして、より堅牢で設計品質の高いコードへと改善することができました。
1.4. その他の品質向上施策
このPRでは、他にも多数の地道な改善を積み重ねています。
-
メーラーのプレビューパス設定: 複数パスを扱える
preview_paths
へ移行し、追記(<<
)で設定することで、既存設定を破壊しない防御的なコーディングを実践しています。 -
テストコードの精密化: リダイレクトテストにおいて、レスポンスボディの曖昧な文字列一致ではなく、
response.redirect_url
を用いてリダイレクト先URLを直接検証。レスポンスボディの内容はUIの変更で容易に変化しうるため、このようなテストは非常に脆くなります。一方で、リダイレクト先のURLという「振る舞い」を直接テストすることで、UIの変更に影響されない、より信頼性の高いテストを構築しています。 -
オートロードパスの最適化:
config.autoload_lib(ignore: %w[assets tasks])
によって、不要なファイルの読み込みを抑制。アプリケーションの起動時間短縮に寄与する、細やかなパフォーマンスへの配慮です。
2. 第二段階:新デフォルト設定の完全適用
第一段階で盤石な基盤を築いた後、いよいよアップグレードプロセスの最終段階へと移行します。
config.load_defaults 7.1
による完全移行
2.1. この作業は、アップグレードプロセスの完了を宣言する、象徴的なステップです。
# 変更前
config.load_defaults 7.0
# 変更後
config.load_defaults 7.1
config/application.rb
のこの一行を更新することで、「本アプリケーションは、これよりRails 7.1の標準仕様に完全に準拠する」ことを明示します。それ以前は「Rails 7.1のgem上で動く、Rails 7.0互換のアプリケーション」でしたが、この変更によって名実ともに「真のRails 7.1アプリケーション」へと生まれ変わります。
そして、この宣言と同時に、移行支援ファイルであったconfig/initializers/new_framework_defaults_7_1.rb
はその役目を終え、プロジェクトから削除されます。全ての新設定は、load_defaults 7.1
によって一括で、かつ整合性を持ってロードされるようになります。
serialize
APIの変更への対応
2.2. load_defaults 7.1
を有効化したことで、新たに対応が必要となったのが、データベースカラムのシリアライズ(オブジェクトを文字列に変換して保存する機能)を担うserialize
メソッドの仕様変更です。
# 変更前
serialize :state_ids, JSON
# 変更後
serialize :state_ids, coder: JSON
coder:
というキーワード引数が必須となりました。これは、シリアライズに用いるライブラリ(この場合はJSON
)を明示的に指定させることで、コードの意図を明確にし、セキュリティリスク(例えば、かつてデフォルトであったYAML
は、デシリアライズ時に任意のコードを実行されうる脆弱性が存在する)を低減するための重要な変更です。
しかし、大規模なプロジェクトでは、既存のserialize
呼び出し全てを一度に修正するのは現実的ではありません。そこで、互換性を維持するための戦略的な一時措置として、以下の設定を導入しました。
config.active_record.default_column_serializer = YAML
これは、「coder:
の指定がない場合は、旧来のYAML
をデフォルトとして使用する」という設定です。この「互換性ブリッジ」を設けることで、既存のコードを破壊することなく安全に移行を進め、修正が必要な箇所を技術的負債として認識しつつ、段階的に、かつ計画的に対応していくことが可能になります。
Discussion