Ruby 2.6、Rails 5.2 からの Ruby 3、Rails 7 へのバージョンアップ

Herokuで稼働させていたRailsアプリケーションが `Heroku-18` のEOLを間近に控えてバージョンアップを余儀なくされたので対応内容を後世のために記しておきます。

## アプリケーションの構成概要
```
Ruby: 2.6.3
Rails: 5.1.7

DB: PostgreSQL
開発環境: Mac、Windows
本番環境: Heroku (Heroku-18)
```

なお、Railsは **APIモード** で実行しています。

```
[config/application.rb]
config.api_only = true
```

## 経緯
Herokuのstackの `Heroku-18` が来年の2023年5月1日でデプロイが行えなくなります。
当初は1つ上の `Heroku-20` への対応を検討していたのですが2年後にまた `Heroku-22` 対応するのはしんどいので一気に `Heroku-22` に上げることを検討しました。

## Heroku-22 に必要なもの
```
Ruby: 3系 (デフォルトは3.1.x)
```

Rubyを3系に上げるにはRailsも含め、主要なgemのバージョンアップが必須でした。
以下はバージョンアップを行ったgemの一覧。

### 変更後のバージョン
```
Ruby: 3.1.2
Rails: 7.0.4
```

### gemの変更差分
|gem|変更前|変更後|
|---|:-:|:-:|
|bundler|1.17.3|2.3.23|
|pg|1.1.4|1.4.4|
|nokogiri|1.10.3|1.12.5|
|msgpack|1.3.0|1.5.6|
|bootsnap|1.4.4|1.12.0|
|active_model_serializers|0.10.9|0.10.13|
|activerecord-import|1.0.4|1.4.1|
|rspec-rails|3.8.2|5.1.2|
|pry-byebug|3.7.0|3.10.1|
|timecop|0.9.1|0.9.5|

以下は Ruby 3、および Rails 7 での仕様変更、実装変更へのコードレベルでの対応になります。

## 1. Ruby 3 対応
### 大文字の予約語を小文字にする
Ruby 3 では命名規約が厳密に適用されるようで予約語を誤って大文字で記述していたものがエラーになりました。
これはRailsのDBマイグレーションスクリプトで真偽値の `false` を誤って `FALSE` と記述していたものがエラーになった例です。

```
[db/migrate/YYYYMMDD_create_hoge.rb]
NameError: uninitialized constant CreateHoge::FALSE

      t.boolean :fuga_flg, default: FALSE
```

### 命名規約エラー
クラス名、モジュール名はキャメルケースで記述するルールですが Ruby 2.x では以下のような命名が許容されていました。

```
HTMLGenerator
```

しかし、Ruby 3.x では読込時にエラーになります。
これはRailsで開発時は動的な読込で気付かなかったのに本番環境に上げたら引っかかるというパターンが多そうです。

```
Unable to load application: NameError: uninitialized constant HTMLGenerator
```

このケースでは `HtmlGenerator` などにリネームする必要があります。

SQL、HTML、XMLなどアッパーケースで記述される技術要素などを含むクラス、モジュールがこれに引っかかりやすそうです。

### キーワード引数の分離
キーワード引数の分離に関しては多くの記事で言及されているので内容自体は触れません。
以下のリンク先などを参照してください。

[プロと読み解く Ruby 3.0 NEWS - キーワード引数の分離](https://techlife.cookpad.com/entry/2020/12/25/155741)

基本的には Hash で渡していたものをキーワード引数に置き換える作業になります。

## 2. Rails 7 対応
### トランザクションブロックで return すると commit されない
Rails 7 ではDBのトランザクションブロックで `return` すると **commit** が実行されずに rollback されます。
`return` を除き、ifブロックに置き換えるなどの対処が必要です。

```ruby
ActiveRecord::Base.transaction do
  # 前略

  return if hoge_condition

  # 後略
end
```

### index_exists? の実装変更
特殊なパターンでハマったのが `ActiveRecord` が提供している **index_exists?** メソッドの実装変更。
3番目の引数のオプション設定の定義が変わっているのでHashで渡していたものはエラーになりました。

* Rails 5.x

```ruby
def index_exists?(table_name, column_name, options = {})
  # 略
end
```

* Rails 7.x

```ruby
def index_exists?(table_name, column_name, **options)
  # 略
end
```

```
index_exists?(:hoge, :column_a, { name: 'hoge_index1' })

⇒ ArgumentError: wrong number of arguments (given 3, expected 2)
```

### Rails のセッション管理の仕様変更への対応
APIモードで実行していると Rails 6.x からは以下のエラーが発生するようになり、リクエストが処理させません。

```
Your application has sessions disabled. To write to the session you must first configure a session store
```

[Rails の API モードでセッションを有効にする](https://daido.hatenablog.jp/entry/2020/05/06/143145)

上記の記事を参考に `config/application.rb` 内で細工を施してやり過ごしました。

```ruby
config.api_only = true

# Rails 6 からのセッション周りの仕様変更への対応ハック
config.middleware.use(ActionDispatch::Cookies)
config.middleware.use(ActionDispatch::Session::CookieStore)
config.middleware.use(ActionDispatch::ContentSecurityPolicy::Middleware)
```