Rails 5.2 から 6.0 にアップグレードしたので手順メモ

弊社はRuby on Railsを中心に扱っている、Webサービス受託開発会社です。

先日、運用中のサービスを Rails 6.0 にアップグレードしました。
私は長くRailsと関わっているのでほぼルーティンになりましたが、誰かの参考になるかもしれないので手順を書き留めておきます。

アップグレード対象によって、追加で作業が必要になるかもしれません。
場合によってはgemの依存関係を解決できずforkしたり、他のgemに置き換えたりすることもあるでしょう。

作業方針の参考程度に捉えていただければと思います。

（といっても、ほとんど [Railsガイドのアップグレードガイド](https://railsguides.jp/upgrading_ruby_on_rails.html) にある通りに作業しただけで、ちょっとした補足をしているだけです）

## 日頃からできているとよいこと

- バージョン管理システムの利用
- 自動テストの整備
- gemのアップデート
- なるべく新しいバージョンのRubyへの対応

### バージョン管理システムの利用

言うまでもなくアップグレードは一筋縄ではいかないことも多いので、少しずつ作業したり、差し戻したり、作業中にほかの作業を取り込んだり･･･といったことも発生します。
また、[アップデートタスク](#4-アップデートタスク)でフレームワークのファイルを更新したとき、差分を確認するのに便利です。

### 自動テストの整備

Railsのアップグレードは大きな変更を伴うものですから、まず壊れます。これを見つけ、正すには自動テストの整備は必須です。

### gemのアップデート

rails gem のアップデートにあわせて、Railsに依存する gem のアップデートが必要になる場合は多いです。
依存gem のアップデートによる影響が最小限になるよう、gemはなるべく最新に保つようにします。

なお、弊社では原則として `Gemfile` でバージョンを縛らない運用をしています。

### なるべく新しいバージョンのRubyへの対応

Railsはリリース時点の最新のRubyをサポートします。
gemと同様に、Rubyのバージョンも追従したほうがよいです。

## アップグレードの実施

### 0. DEPRECATION WARNING 潰し

Rails は将来互換性がなくなる機能を利用しているとき、警告と移行方法を提示してくれます。
アップグレード前に壊れることがわかるので、アップグレードの前に修正しておきます。

### 1. gemのアップデート

各gemを可能な限りアップデートして、正しく動作することを確認しておきます。

### 2. Rubyのアップデート

アップデート前、アップデート後両方が動くRubyバージョンのうち、最新のものにアップデートして正しく動作することを確認しておきます。
今回は Ruby 2.6 にしました。

### 3. rails gem のアップデート

[Rails アップグレードガイド](https://railsguides.jp/upgrading_ruby_on_rails.html)に従って作業を進めていきます。

```ruby
# before: gem 'rails', '~> 5.2'
gem 'rails', '~> 6.0'
```

```bash
$ bundle update rails
```

rails gemのバージョンが変化することで解決できなくなる依存関係が列挙されるので、それぞれ確認ます。

```
Fetching gem metadata from http://rubygems.org/.........
Fetching gem metadata from http://rubygems.org/.
Resolving dependencies.....
Bundler could not find compatible versions for gem "railties":
  In Gemfile:
    devise was resolved to 4.7.0, which depends on
      railties (>= 4.1.0)

    jquery-rails was resolved to 4.3.5, which depends on
      railties (>= 4.2.0)

    letter_opener_web was resolved to 1.3.4, which depends on
      railties (>= 3.2)

    meta_request was resolved to 0.7.2, which depends on
      railties (>= 3.0.0, < 7)

    rails (~> 6.0) was resolved to 6.0.0, which depends on
      railties (= 6.0.0)

    rails-i18n was resolved to 5.1.3, which depends on
      railties (>= 5.0, < 6)

    rspec-rails was resolved to 3.8.2, which depends on
      railties (>= 3.0)

    sassc-rails was resolved to 2.1.2, which depends on
      railties (>= 4.0.0)

    web-console was resolved to 3.7.0, which depends on
      railties (>= 5.0)
```

rails 6.0 が railties 6.0.0 を要求しているものの、
rails-i18n 5.1.3 が railties 6 未満を要求していることがわかります。
ここで依存性の重複が発生したようです。

[Bundler: bundle update - Overlapping Dependencies](https://bundler.io/man/bundle-update.1.html#OVERLAPPING-DEPENDENCIES)

rails-i18n を確認してみます。

[https://rubygems.org/gems/rails-i18n](https://rubygems.org/gems/rails-i18n)

すると、rails-i18n 6.0.0 という、 rails 6.0 に対応したものがリリースされていました。

[https://rubygems.org/gems/rails-i18n/versions/6.0.0](https://rubygems.org/gems/rails-i18n/versions/6.0.0)

rails と一緒に rails-i18n をアップデートしてみます。

```
$ bundle update rails railties rails-i18n
```

無事 rails gem のアップデートができました。

```
Fetching gem metadata from http://rubygems.org/.........
Fetching gem metadata from http://rubygems.org/.
Resolving dependencies.....
(snip)
Using activesupport 6.0.0 (was 5.2.3)
(snip)
Using activemodel 6.0.0 (was 5.2.3)
(snip)
Using activejob 6.0.0 (was 5.2.3)
Using activerecord 6.0.0 (was 5.2.3)
(snip)
Using actionview 6.0.0 (was 5.2.3)
(snip)
Using actionpack 6.0.0 (was 5.2.3)
(snip)
Using activestorage 6.0.0 (was 5.2.3)
Using actionmailer 6.0.0 (was 5.2.3)
(snip)
Using actioncable 6.0.0 (was 5.2.3)
(snip)
Using railties 6.0.0 (was 5.2.3)
(snip)
Using rails 6.0.0 (was 5.2.3)
Using rails-i18n 6.0.0 (was 5.1.3)
(snip)
Bundle updated!
```

### 4. アップデートタスク

アップデートタスクを実行します。

```bash
$ bundle exec rails app:update
```

途中 conflict が報告され、上書きしてよいか確認してくれます。
適切にバージョン管理されていれば上書き後差分を確認できますし、戻すこともできるので、すべて上書きします。

```
    conflict  config/boot.rb
Overwrite /home/takeyuweb/Projects/myapp/config/boot.rb? (enter "h" for help) [Ynaqdhm] a
       force  config/boot.rb
       exist  config
    conflict  config/routes.rb
       force  config/routes.rb
    conflict  config/application.rb
       force  config/application.rb
   identical  config/environment.rb
    conflict  config/cable.yml
       force  config/cable.yml
    conflict  config/puma.rb
       force  config/puma.rb
   identical  config/storage.yml
       exist  config/environments
    conflict  config/environments/development.rb
       force  config/environments/development.rb
    conflict  config/environments/production.rb
       force  config/environments/production.rb
    conflict  config/environments/test.rb
       force  config/environments/test.rb
       exist  config/initializers
   identical  config/initializers/application_controller_renderer.rb
    conflict  config/initializers/assets.rb
       force  config/initializers/assets.rb
   identical  config/initializers/backtrace_silencers.rb
    conflict  config/initializers/content_security_policy.rb
       force  config/initializers/content_security_policy.rb
   identical  config/initializers/cookies_serializer.rb
      create  config/initializers/cors.rb
   identical  config/initializers/filter_parameter_logging.rb
   identical  config/initializers/inflections.rb
    conflict  config/initializers/mime_types.rb
       force  config/initializers/mime_types.rb
      create  config/initializers/new_framework_defaults_6_0.rb
   identical  config/initializers/wrap_parameters.rb
       exist  config/locales
    conflict  config/locales/en.yml
       force  config/locales/en.yml
      remove  config/initializers/cors.rb
       exist  bin
   identical  bin/rails
   identical  bin/rake
    conflict  bin/setup
       force  bin/setup
   identical  bin/yarn
       rails  active_storage:update
Copied migration 20190829054009_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.active_storage.rb from active_storage

After this, check Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html for more details about upgrading your app.
```

更新のあったファイルを個別に確認して、必要に応じて編集したり、差し戻したりしていきます。
たとえば routes.rb を上書きした場合は、ルーティングの記述を復活させる必要があるでしょう。
差分を確認しながら進めていきます。

#### config.load_defaults について

アップグレード時はなるべく変更が小さくなるように努めるべきだと考えます。
まずは従来のバージョンのデフォルト設定を使うようにします。

```ruby
config.load_defaults 5.2
```

#### Rails 5.2 => 6.0 固有の内容について

[Rail アップグレードガイド - Rails 5.2からRails 6.0へのアップグレード](https://railsguides.jp/upgrading_ruby_on_rails.html#rails-5-2%E3%81%8B%E3%82%89rails-6-0%E3%81%B8%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89)
 を参考に作業を進めます。

今回はCookieの互換性がいますぐ失われるのを避けたかったので、以下を `new_framework_defaults_6_0.rb` で設定しました。

```ruby:new_framework_defaults_6_0.rb
Rails.application.config.action_dispatch.use_cookies_with_metadata = false
```

また、Action Cable を使用していたのでそちらの対応を行いました。

#### オートローダーについて

Rails 6.0 からはオートローダーが Zeitwerk になり、オートロードの挙動が変わっています。

[定数の自動読み込みと再読み込み (Zeitwerk)](https://railsguides.jp/autoloading_and_reloading_constants.html)

config.load_defaults 5.2 では従来のものが使われるので、いったん置いておきます。
従来のオートローダーで正しく動くことを保証できるようになってから、切り替えることを考えることにします。

## テスト実行

```bash
$ bundle exec rails test
# または
$ bundle exec rspec
```

おそらく大量のテストが失敗するでしょう。
地道に修正していきます。（大抵は変な書き方をしてたり、隠れてたバグが表面化したり、といったケーススだったりします）

### rspec-rails を使っている場合

Rails 6.0 対応の rspec-rails は4系になりますが、この記事の作成時点でベータ版（ 4.0.0.beta2 ）のため、 bundle update するだけでは入りません。`Gemfile` を触ります。

[https://rubygems.org/gems/rspec-rails](https://rubygems.org/gems/rspec-rails)

```ruby:Gemfile
gem 'rspec-rails', '4.0.0.beta2'
```

```bash
$ bundle update rspec-rails
```

## 今後やっていくこと

### DEPRECATION WARNING 潰し

Rails 6.1 で互換性がなくなる機能を使っていると警告が表示されるので修正していきます。

> DEPRECATION WARNING: Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1. To continue case sensitive comparison on the :beacon attribute in User model, pass `case_sensitive: true` option explicitly to the uniqueness validator.

### config.load_defaults 6.0 にする

今回 `new_framework_defaults_6_0.rb` に `Rails.application.config.action_dispatch.use_cookies_with_metadata = false` を設定しました。
検討や必要に応じて修正を進め、 `new_framework_defaults_6_0.rb` を削除できるようになれば、晴れて `config.load_defaults 6.0` にでき、Rails 6.0 対応を終えたといえるでしょう。

### Zeitwerk に対応させる

config.load_defaults 6.0 にした後も、当初は従来と同じオートロードの挙動になるようにするには、以下のようにします。

```ruby:application.rb
config.autoloader = :classic
```

ドキュメントに従って、従来のオートローダーから新しいオートローダーである Zeitwerk に対応するための修正を行っていきます。

[Rails 5.2からRails 6.0へのアップグレード オートローディング](
https://railsguides.jp/upgrading_ruby_on_rails.html#%E3%82%AA%E3%83%BC%E3%83%88%E3%83%AD%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0)

## 終わりに

Railsに限らず、アップグレードのコツは、溜めないことだと思っています。

個人的には、Rails 5.2 から 6.0 へは、メジャーバージョンアップにしては楽だと感じました。

皆さんも、サックリとアップグレードを済ませて、6.1 の登場を待ちましょう。