Railsを5.2.4から6.0.4にアップデートした手順をメモ

Railsを5.2.4→6.0.4にアップデートする機会があった。アップデートのために行った手順、手順ごとの調査をメモした。

手順

1. Rails以外のバージョンが古いライブラリをアップデートする

Railsアップデートの影響範囲を小さくするため、Rails以外のライブラリをアップデートしておく。 特にメジャーバージョンが古いライブラリは上げておく。 全部のバージョンをpatchまで最新にしようとするとキリがないので、そこはラフに判断する。

2. gemfileをrails 6.0.4 にして、bundle update rails を実行し失敗の原因となったgemを最新にする

3. rails を6.0.4 にアップデートする

gem 'rails', '6.0.4'

bundle update rails して成功することを確認する。

4. rails app:update する

app:updateとは

Rails アップグレードガイド_1.4 アップデートタスク

Gemfileに記載されているRailsのバージョンを更新後、このコマンドを実行することで、新しいバージョンでのファイル作成や既存ファイルの変更を対話形式で行うことができます。

app:updateを実行する

m(migrate)を打ってエディタで差分を確認したが、今回は修正すべき内容が特に見当たらなかった。

5. RailsガイドのRails 5.2からRails 6.0へのアップグレードを読む

Rails 5.2からRails 6.0へのアップグレード 以下、特に気になった項目を列挙。

npmの全パッケージが@railsスコープに移動

Rails 5.2からRails 6.0へのアップグレード

これまで「actioncable」「activestorage」「rails-ujs」パッケージのいずれかをnpmまたはyarn経由で読み込んでいた場合は、これらを6.0.0にアップグレードする前にそれらの依存関係の名前を以下のように更新しなければなりません。

rails-ujsであれば rails-ujsから@rails/ujsへ変更する

Rails 6のアップデートに先立ってこちらを修正してリリースした。

セキュリティ向上のためpurposeとexpiryメタデータが署名済みおよび暗号化済みCookieに埋め込まれるようになった

cookieを引き続きRails 5.2以前でも読み取れるようにする必要がある場合や、6.0のデプロイを検証中で前のバージョンに戻せるようにしたい場合は、Rails.application.config.action_dispatch.use_cookies_with_metadataにfalseを設定してください。

Rails 6.0から5.2へ巻き戻せるようにしたいので設定する。

purposeについては以下を参考にした。

Rails 6のcookieに「purpose」メタデータが追加(翻訳)|TechRacho by BPS株式会社

autoloaderがclassicからzeitwerkに変更される

アップデートの影響を小さくするため、ひとまずclassicのままRails 6.0へアップデートすることにした。

Rails アップグレードガイド_3.7.15 Rails 6でclassicモードのオートローダーを使う方法

6. 動かして出てきた不具合を修正する

rails sしてログを見たり、自動テストの結果を見たり、アプリケーションを触って出てきた不具合を修正していく。

a. hamlit-rails 周りのDEPRECATION WARNING

W, [2021-09-09T10:57:35.117155 #87570]  WARN -- : DEPRECATION WARNING: Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
  >> #<Hamlit::RailsTemplate:0x00007fef2f58f2c0>.call(template)
To:
  >> #<Hamlit::RailsTemplate:0x00007fef2f58f2c0>.call(template, source)

参考: Rails 6 を動かす際に "DEPRECATION WARNING: Single arity template handlers are deprecated." という警告が出た場合の対処

bundle update hamlit-railsを実行し HamlをアップデートしたらDEPRECATION WARNINGは消えた。

こちらはrails6.0アップデートに先立ってリリースした。

b. RailsのInitialize中にautoloadを使ったクラスの読み込みで警告が出る

ログ

W, [2021-09-09T11:11:44.347319 #88314]  WARN -- : DEPRECATION WARNING: Initialization autoloaded the constant FooBar.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload FooBar, for example,
the expected changes won't be reflected in that stale Class object.

`config.autoloader` is set to `classic`. This autoloaded constant would have been unloaded if `config.autoloader` had been set to `:zeitwerk`.

Please, check the "Autoloading and Reloading Constants" guide for solutions.

ソース

原因

RailsのInitialize中はautoloadが有効でないため。

Zeitwerkの壊し方 - Qiita

initialiizers実行時はZeitwerkのオートロードが有効になっていません。 Railsは一連のinitializerを実行した最後に、Rails::Application::Finisherを呼び出しますが、ここで初めてZeitwerkが起動する仕組みになっています。

解決策

クラス読み込みが失敗したファイルにrequireを追加すれば解決する。

require './path/to/fobar'

あるいは以下の記事にあるように config.after_initialize を使ってもうまくいく。 STORES Rails アプリを Zeitwerk 有効化するまでの道のり - STORES Tech Blog

ログにある通り、initializerで何らかのクラスを変数にセットしている場合、それらはautoloadしても更新されない(stale Class objectになる) 点に注意する

Reloading does not reboot the application, and therefore code executed during initialization does not run again. So, if you reload FooBar, for example, the expected changes won't be reflected in that stale Class object.

参考: 定数の自動読み込みと再読み込み (Zeitwerk)_6.1 古くなったオブジェクトの再読み込み

c. Rails 6.0から入ったenum negative scope がアプリケーションで定義したscopeをoverrideしている

W, [2021-09-09T12:20:28.272948 #90420] WARN -- : Creating scope :not_deleted. Overwriting existing method FooBar.not_deleted.

ネガティブスコープの影響 https://railsguides.jp/6_0_release_notes.html

すべてのenum値についてネガティブスコープを追加 (Pull Request) https://github.com/rails/rails/pull/35381

動作検証したところ、アプリケーションで定義したFooBar.not_deletedはoverrideされ、enum negative scopeが呼ばれていた。 今回のケースでは、どちらのスコープでも実行されるsqlが全く一緒だったので、アプリケーションで定義したFooBar.not_deletedを削除することで解決した。

d.config_forから返されるハッシュのsliceメソッドの引数にシンボルでなく文字列でアクセスするとエラー

configuration.foo_barのkeyが5.2系ではstringだったのに6.0系ではsymbolになっている。

https://railsguides.jp/6_0_release_notes.html#railties-%E9%9D%9E%E6%8E%A8%E5%A5%A8%E5%8C%96

config_forから返されるハッシュにシンボルでないキーでアクセスすることを非推奨化 (Pull Request)

Hash.[]ならstring keyでもアクセスできるが、Hash.sliceだと非推奨化じゃなくて取得できなかった

6.0のログ

Loading development environment (Rails 6.0.4)
[1] pry(main)> Rails.configuration.foo_bar
=> {:a=>[1, 2], :b=>[3], :c=>[4, 5, 6, 7]}
[2] pry(main)> Rails.configuration.foo_bar.slice('a','b').values.flatten.sort
=> []
[3] pry(main)> Rails.configuration.foo_bar.slice('a','b')
=> {}
[4] pry(main)> Rails.configuration.foo_bar.slice(:a,:b)
=> {:a=>[1, 2], :b=>[3]}

[1] pry(main)> Rails.configuration.foo_bar['a'] # [] のアクセスなら取得できる
=> [1, 2]
[2] pry(main)> Rails.configuration.foo_bar.slice('a') # slice だと取得できない
=> {}

[3] pry(main)> Rails.configuration.foo_bar.class
=> ActiveSupport::OrderedOptions # https://api.rubyonrails.org/v6.0.4/classes/ActiveSupport/OrderedOptions.html string key を symbol に変換する処理あり

# ['string key']['string key'] とすると DEPRECATION WARNINGが出た
[8] pry(main)> Rails.configuration.repro['a']['b']
DEPRECATION WARNING: Accessing hashes returned from config_for by non-symbol keys is deprecated and will be removed in Rails 6.1. Use symbols for access instead. (called from <main> at (pry):8)
W, [3-09-10T12:26:05.948315 #5023]  WARN -- : DEPRECATION WARNING: Accessing hashes returned from config_for by non-symbol keys is deprecated and will be removed in Rails 6.1. Use symbols for access instead. (called from <main> at (pry):8)

5.2のログ

D, [3-09-10T11:01:22.410693 #3695] DEBUG -- :    (8.2ms)  SET NAMES utf8mb4 COLLATE utf8mb4_general_ci,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
Loading development environment (Rails 5.2.6)
[1] pry(main)> Rails.configuration.foo_bar
=> {"a"=>[1, 2], "b"=>[3], "c"=>[4, 5, 6, 7]}
[3] pry(main)> Rails.configuration.foo_bar.slice('a','b').values.flatten.sort
=> [3, 1, 2]

[2] pry(main)> Rails.application.config_for(:foo_bar).class
=> Hash # 6.0.4だとActiveSupport::OrderedOptionsに変わっている

解決策

sliceしている箇所はstring_keyからsymbol_keyにした。 他、怪しそうな箇所がちゃんと動いているか調べた。

d. ActiveRecordインスタンスの配列のキャッシュのデシリアライズに失敗する

Rails.cache.fetch('some_key') do
   FooBar.scope_method.to_a
end

のようにActiveRecordインスタンスの配列をシリアライズして保存していた。 これのでシリアライズに失敗するようになった。

原因

Rails 5 -> 6でシリアライズの形式が変わったためだと思われる。

解決策

cacheのkeyの末尾にバージョンを入れ、Rails 6系の時にRails 5系のキャッシュへのヒットを避けた。

Rails.cache.fetch("some_key_#{Rails.configuration.cache.version}") do
   FooBar.scope_method.to_a
end
採用しなかった解決策
  • versionオプションを使う
    • https://api.rubyonrails.org/v5.2.5/classes/ActiveSupport/Cache/Store.html#method-i-fetch
    • Setting :version verifies the cache stored under name is of the same version. nil is returned on mismatches despite contents. This feature is used to support recyclable cache keys.

    • versionが存在しない場合は別バージョンと認識されなかったため
    • 今回はversionオプションを使わなかったが、メンテを継続するにあたってはversionオプションを利用するのが良いと思う。
      • FooBarにメソッドやインスタンス変数が追加されるとデシリアライズに失敗するリスクがあるので、そういった場合に備えて適切にバージョン管理する仕組みが必要であるため。

感想

自動テストを書いておくことがとても重要。上記に挙げた不具合の一部は、自動テストのおかげで発見できた。

自動テストによる検証が大切なことは、以下にも記述されている。 Rails アップグレードガイド - Railsガイド

アップグレード後にアプリケーションが正常に動作していることを確認する方法としては、良いテストカバレッジをアップグレード前に準備しておくのが最善です。アプリケーションを一気に検査する自動テストがないと、変更点をすべて手動で確認しなければならず膨大な時間がかかってしまいます。Railsのようなアプリケーションの場合、これはアプリケーションのあらゆる機能を一つ残らず確認しなければならないということです。アップグレードの実施は、テストカバレッジをきちんと準備してから行なうよう、くれぐれもお願いします。