config.load_defaultsとnew_framework_defaults_x_x.rbの関係を詳しく調べてみた

# config.load_defaultsとnew_framework_defaults_x_x.rbの関係を詳しく調べてみた

## TL; DR（はじめに結論）

以下はRails 5.1から5.2にアップグレードしたケースを想定しています。
必要に応じてバージョン番号を読み替えてください。

- Rails 5.2アップグレード後に`config.load_defaults 5.2`と書くと、Rails 5.2のデフォルトの挙動になる
- 一部にRails 5.1の挙動に戻したい項目があれば、`new_framework_defaults_5_2.rb`の該当項目のコメントを外し、そのうえで設定値を論理反転させる（`true`は`false`へ、`false`は`true`へ修正する）
- Rails 5.1の挙動に戻す必要がなければ、`new_framework_defaults_5_2.rb`は削除しても構わない

## はじめに
既存のRailsアプリを新しいバージョンにアップデートするために `rails app:update` コマンドを使うと、 `config/initializers` 以下に `new_framework_defaults_5_2.rb` のようなファイルが作成されます。

その一方で、`config/application.rb` を見ると、`config.load_defaults 5.1`のような設定も存在します。

Railsをアップデートしたときは、これら2つの設定ファイルをどのように使うのが良いのでしょうか？
僕自身、あまりしっかり理解しないまま使ってきてたので、詳しく調べてみました。

## この記事で想定するRailsアプリケーション

この記事では以下のようなRailsアプリケーションを想定します。

- Rails 5.1の時代にrails newした
- その後、Rails 5.2にアップグレードした
- アップグレード時には `rails app:update` コマンドを使った（つまり、 `config/initializers/new_framework_defaults_5_2.rb` が作成された）
- `config/application.rb` の `config.load_defaults` は `5.1` のまま

```ruby:Gemfile
# ...
gem 'rails', '5.2.3'
# ...
```

```ruby:config/initializers/new_framework_defaults_5_2.rb
# Be sure to restart your server when you modify this file.
#
# This file contains migration options to ease your Rails 5.2 upgrade.
#
# Once upgraded flip defaults one by one to migrate to the new default.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.

# Make Active Record use stable #cache_key alongside new #cache_version method.
# This is needed for recyclable cache keys.
# Rails.application.config.active_record.cache_versioning = true

# Use AES-256-GCM authenticated encryption for encrypted cookies.
# Also, embed cookie expiry in signed or encrypted cookies for increased security.
#
# This option is not backwards compatible with earlier Rails versions.
# It's best enabled when your entire app is migrated and stable on 5.2.
#
# Existing cookies will be converted on read then written with the new scheme.
# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true

# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
# Rails.application.config.active_support.use_authenticated_message_encryption = true

# Add default protection from forgery to ActionController::Base instead of in
# ApplicationController.
# Rails.application.config.action_controller.default_protect_from_forgery = true

# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
# 'f' after migrating old data.
# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true

# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
# Rails.application.config.active_support.use_sha1_digests = true

# Make `form_with` generate id attributes for any generated HTML tags.
# Rails.application.config.action_view.form_with_generates_ids = true
```

```ruby:config/application.rb
module SampleApp
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 5.1
  end
end
```

## 調査対象の設定値
この記事では `Rails.application.config.action_view.form_with_generates_ids` に着目します。

この設定値は `form_with` メソッドでフォームを作成した際に、各入力項目に `id` 属性を自動的に付与するかどうか決める設定値です。

```erb:app/views/users/_form.html.erb
<%= form_with(model: user, local: true) do |form| %>
  <!-- ... -->
  <%= form.text_field :email %>
  <!-- ... -->
<% end %>
```

```html 
<form action="/users" ...>
  <!-- ... -->
  <input type="text" name="user[email]">
  <!-- または -->
  <input id="user_mail" type="text" name="user[email]">
  <!-- ... -->
</form>
```

## 調査結果

`config.load_defaults` と `new_framework_defaults_5_2.rb` の `form_with_generates_ids` の設定値の組み合わせによって、HTMLの出力内容がどうなるか調査したところ、次のような結果になりました。

|           |      |   |       |   |   |       |
|-------------------------|----------|------|----------|------|------|----------|
| load_defaults           | 5.1      | 5.1  | 5.1      | 5.2  | 5.2  | 5.2      |
| form_with_generates_ids | コメントアウト| true | false    | コメントアウト | true | false    |
| id属性の有無                  | 付かない | 付く | 付かない | 付く | 付く | 付かない |

### わかったこと

上の結果からわかったことは以下のとおりです。

- `load_defaults`が5.1で、`form_with_generates_ids`がコメントアウトされていると、Rails 5.1相当の挙動になる
- `load_defaults`が5.2で、`form_with_generates_ids`がコメントアウトされていると、Rails 5.2で導入された新しいデフォルトの挙動になる
- `form_with_generates_ids`を`true`または`false`にすると、`load_defaults`の挙動を上書きすることができる

また、`rails app:update`コマンドを実行して生成される`new_framework_defaults_5_2.rb`は、「Rails 5.2のデフォルトの挙動にするための設定」がコメントアウトされています。

```ruby:config/initializers/new_framework_defaults_5_2.rb
# trueにするとRails 5.2のデフォルトの挙動に一致する
# Rails.application.config.action_view.form_with_generates_ids = true
```

## 実際にはどう活用するのがよいか

では実際のアップグレード作業ではどのように活用するのがよいでしょうか？

理想的には次のような手順になると思います。

1. Rails 5.2にアップグレードし、`rails app:update`コマンドを実行する
2. `load_defaults`は`5.1`のままで、`new_framework_defaults_5_2.rb`のコメントを1つずつ外し（つまり、Rails 5.2のデフォルトの挙動に変えて）、動作確認する
3. `new_framework_defaults_5_2.rb`のコメントを全部外しても問題が起きなければ、`load_defaults`を`5.2`に変更し、`new_framework_defaults_5_2.rb`を削除する


```ruby:config/application.rb
module SampleApp
  class Application < Rails::Application
    # Rails 5.2のデフォルトの挙動で問題ないので5.1から5.2に変更
    config.load_defaults 5.2
  end
end
```

```ruby:config/initializers/new_framework_defaults_5_2.rb
# 不要なので削除
```

### 新しいデフォルトの挙動では一部都合が悪い場合

一方、Rails 5.2のデフォルトの挙動では問題が起きる場合（たとえば、どうしてもid属性を自動的に付けたくない場合）は、次のような方法を取ることになるはずです。

1. `load_defaults`を`5.2`に変更する
2. `form_with_generates_ids`の設定値を`true`ではなく`false`にする（Rails 5.1相当の挙動にする）
3. その他の項目（Rails 5.2のデフォルトの挙動に変えても問題ない項目）については、`new_framework_defaults_5_2.rb`の設定をコメントアウト（または削除）する

```ruby:config/application.rb
module SampleApp
  class Application < Rails::Application
    # いったんすべてのデフォルトの挙動をRails 5.2相当にする
    config.load_defaults 5.2
  end
end
```

```ruby:config/initializers/new_framework_defaults_5_2.rb
# 5.2のデフォルトの挙動では問題がある項目のみ、設定を上書きする
# （コメントを外すだけでなく、trueからfalse、またはfalseからtrueへ、というように、コメントを外す前の設定値を論理反転させる）
Rails.application.config.action_view.form_with_generates_ids = false

# 他の項目はRails 5.2のデフォルトでいいので削除
```

## load_defaultsについてもう少し詳しく

### 過去のバージョンのデフォルト設定も同時に読み込まれる

`load_defaults`で読み込んだデフォルト設定は、過去のRailsのバージョンのデフォルト設定も同時に読み込みます。

つまり、`config.load_defaults 5.2`と書いた場合は、以下のようにRails 5.2だけでなく、5.1と5.0のデフォルト設定値も反映されます。

Rails 5.0のデフォルトの設定値を反映
　↓
Rails 5.1のデフォルトの設定値を反映
　↓
Rails 5.2のデフォルトの設定値を反映

なお、上で示したように、この仕組みが使われるのはRails 5.0までです。

`load_defaults`メソッドの実装はそこまで複雑ではないので、詳細な仕様を理解したい場合はソースコードを直接読むのが良いと思います。

https://github.com/rails/rails/blob/v5.2.3/railties/lib/rails/application/configuration.rb#L65-L122

### load_defaultsもnew_framework_defaults_x_xも指定しない場合はどうなるのか

`config.load_defaults`も`new_framework_defaults_x_x`もどちらも何も指定しない場合は、どの設定値が使われるのか予想が付きません。

たとえば、`form_with_generates_ids`はRails 5.1相当の挙動になりましたが、`default_protect_from_forgery`はRails 5.2相当の挙動になりました。

このように、挙動の予想が付かなくなるので、`load_defaults`には何らかの設定を入れておく方が良いでしょう。

```ruby:config/application.rb
module SampleApp
  class Application < Rails::Application
    # こういうことをしてはいけない！！（load_defaultsを指定しない場合は、挙動の予想が付かない）
    # config.load_defaults 5.2
  end
end
```

## 参考文献

- [Rails アップグレードガイド \- Rails ガイド](https://railsguides.jp/upgrading_ruby_on_rails.html)
- [Rails 5\.1で追加されたconfigまとめ \| 日々雑記](https://y-yagi.tumblr.com/post/160114728555/rails-51%E3%81%A7%E8%BF%BD%E5%8A%A0%E3%81%95%E3%82%8C%E3%81%9Fconfig%E3%81%BE%E3%81%A8%E3%82%81)
- [ruby on rails \- How does "config/initializers/new\_framework\_defaults\_5\_2\.rb" work? \- Stack Overflow](https://stackoverflow.com/questions/50892572/how-does-config-initializers-new-framework-defaults-5-2-rb-work)