Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails 6の新しいデフォルト設定と安全な移行方法を詳しく解説(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 6の新しいデフォルト設定と安全な移行方法を詳しく解説(翻訳)

本記事では、rails app:updateで生成されるnew_framework_defaults_6_0.rbの9つのデフォルトフラグについてひととおり解説します。最後までお読みいただければ、自信を持ってこのファイルを削除し、application.rbload_defaults 6.0と堂々と書けるようになります。

本記事ではRails 5.2のデフォルト設定を使っているアプリを前提としています。これについては、自分のアプリのapplication.rbload_defaults 5.2と書かれているかどうかを調べることで確認できます。

訳注: 簡単のため、以下のデフォルト設定ではRails.application.config.を省略しています。

1. action_view.default_enforce_utf8 = false

フラグの機能

formのaccept-charset属性は、IE5やその時代のブラウザで導入されました。この属性は、送信するform要素のデータで用いるエンコーディングをブラウザに指定します。Railsはやがてaccept-charset="UTF-8"を自動的に追加してUTF-8での送信をブラウザに強制するようになりました。

しかしIE 5には困った振る舞いがいくつかありました。送信されたすべての文字がIE 5のデフォルトcharsetで表現できる場合は、accept-charsetを無視するのです。ブラウザのデフォルトcharsetがLatin-1に設定されているユーザーがいるとしましょう(訳注: Latin-1はフランス語やドイツ語などのヨーロッパ系言語で用いられていました)。このユーザーがIE 5からフォームを送信したとき、入力をすべてLatin-1で表現できてしまう場合、accept-charset属性は無視され、Latin-1でエンコードされたフォームが送信されます。つまりUTF-8のデータベースにLatin-1の文字が入ってしまうのです!

Railsではこれを回避するハックとして、2010年の@25215d7[:_snowman_]というHTMLエンティティがhidden formフィールドに追加されました。他のエンコードではサポートされていないUnicodeのsnowman絵文字☃を用いて、IE 5にaccept-charset属性を守らせます。

雑学: このsnowmanはその後、ログに雪だるまの絵文字が出力されるのを嫌がる人たちがいることからチェックマーク「✓」に変更されました。

参考: Rails の `utf8=✓` の歴史と消し方と snowman ☃ - Qiita

このチェックマーク記号は未だにRailsフォームのhiddenフィールドに居座っています。このフラグのコメントを外すと、チェックマーク記号はフォームに含まれなくなります。

このフラグを安全に有効にするには

accept-charsetのすっとこどっこいな振る舞いはIE 5からIE 8まで続きました。これらのブラウザをサポートしなくてもよければ、コメントを外しても安全です。

2. action_dispatch.use_cookies_with_metadata = true

フラグの機能

Railsはcookie値を暗号化および署名し、cookie値の暗号化を解除して署名を照合することで、悪者によって改変されていないことを確認します。しかしこの方法では、一部のcookieで暗号化済みおよび署名済みの値を悪者がコピーして、他のcookieの値として用いることまでは防げません。

次のシナリオで考えてみましょう。

  1. Railsに次の2つのcookieが設定されているとする
    • is_admin: false
    • is_a_doofus: true(doofus: アホ)
  2. 2つのcookieの暗号化/署名済みの値を悪者が入れ替える
  3. Railsはリクエストに含まれる値を読み取って「よろしい、値は改変されていない」と承認する
  4. Railsはユーザーを「adminかつdoofusでない」と認識するが、実際は「非adminかつdoofus」

フラグの話に戻ります。このフラグをコメント解除すると、「purpose」というフィールドをcookieに埋め込んでから、暗号化と署名を行うようになります。するとRailsは、上のステップ3で「ふむ、値はどちらも改変されてはいないが、cookieの名前とpurposeが一致しておらぬな」と判断します。かくして悪人は相変わらずdoofusのままです。

このフラグを安全に有効にするには

このフラグをコメント解除しても既存のcookieが壊れることはありません。cookieはリクエスト時に読み込まれ、レスポンスではpurposeフィールドを追加してリライトされます。以後、新しいcookieにはpurposeフィールドが含まれるようになります。

# This option is not backwards compatible with earlier Rails versions.
# It's best enabled when your entire app is migrated and stable on 6.0.
大意: このオプションは従来のRailsと互換性がありません。アプリ全体の移行が終わって6.0で安定運用してから有効にするのがベストです。

恐ろしげなwarningが表示されますが怖がることはありません。このフラグをいったんコメント解除すると、アプリがRails 6.xにロックインされますよと言っているのです。Rails 5.xはcookieのpurposeフィールドを解釈できないので、Rails 5.xへのダウングレードは不可能になります。

アプリがRails 6で安定運用できることを確認できれば、このフラグは安全にコメント解除できます。

3. action_dispatch.return_only_media_type_on_content_type = false

フラグの機能

HTTPレスポンスにはcontent-typeが含まれています。RailsがHTMLビューをレンダリングすると、content-type: text/htmlのように設定されます。このヘッダーには"text/html"というメディアタイプだけが含まれていることにご注目ください。

このフラグをコメント解除すると、このヘッダーに他の値も追加されるようになります。デフォルトではcontent-type: text/html; charset=utf-8のようにcharsetが含まれます。

このヘッダーの値は、以前からActionDispatch::Response#content_typeで調べられるようになっていますが、この値にもcharsetが含まれるようになります。

このフラグを安全に有効にするには

このフラグをコメント解除すると、外部と内部の両方に影響が生じます。

外部向けとしては、アプリのユーザーがレスポンスで完全なcontent-typeヘッダーを受け取るようになります。ユーザーがcontent-typeをあれこれ調べるようなことがなければ、これといった問題はないと考えても大丈夫です。

内部では、ActionDispatch::Response#content_typeの値が従来とは異なるものになります。影響が生じるかどうかはテストスイートでわかるでしょう。テストがコケる場合は、以下のような博物館入りのcontroller specが使われているかもしれません。

Failure/Error: expect(response.content_type).to eq("text/html");
expected: "text/html"
            got: "text/html; charset=utf-8"
(compared using ==)
     # ./spec/controllers/users_controller_spec.rb:10:in `block (4 levels) in <top (required)>'

修正の指針としては以下が考えられます。

  • expect(response.media_type).to eq("text/html")にする
  • 従来のcontent_typeと同様にmedia_typeメソッドがメディアタイプだけを返すようにする
  • expect(response.content_type).to include("text/html")にする
  • コントローラのテストをシステムテストにリファクタリングすることを検討する(コントローラのテストは今や非推奨です)

4. active_job.return_false_on_aborted_enqueue = true

フラグの機能

Active Jobにはbefore_enqueueなどのライフサイクルコールバックがあることを、きっと皆さまもご存知でしょう。しかしActive Jobのどの場所であろうと、throw(:abort)を実行した瞬間に処理が止まって終了することや、そうしたコールバックの中で以下のように書けることはご存知ですか?

class MyJob < ApplicationJob
  before_enqueue { |job| throw(:abort) if job.arguments.first }
  def perform; end
end

job1 = MyJob.perform_later(false)
job2 = MyJob.perform_later(true)

job1は、フラグの設定にかかわらず、キューに入ったジョブクラスのインスタンスになります。現時点ではjob2も同様です。そしてこのフラグをコメント解除すると、コールバック内でabortがスローされてjob2falseになります。

このフラグを安全に有効にするには

コードベースでabortをgrepしてください。abortのインスタンスを調べて、ジョブのコールバックにabortが出現しないようにしましょう。

ジョブのコールバックでabortを見つけてしまったら、ジョブがどこでキューに入ったかを調べる必要があります。常にジョブインスタンスにするために、その箇所がperform_laterに依存しないようにします。運悪くperform_laterに依存していたら、falseでもやっていけるようにリファクタリングする必要があります。

5. active_storage.queues.analysis = :active_storage_analysis

フラグの機能

Active Storageでファイルがアタッチされると、ActiveStorage::Attachmentafter_commit_createコールバックが呼び出されます。このコールバックによってActiveStorage::AnalysisJobがキューに入ります。そして実行されると、このジョブでActiveStorage::Blob#analyzeが呼び出されます。このメソッドは、ファイルからメタデータを抽出するのにプラグインシステムを用いています。このメタデータは、blobレコードのmetadataカラムに保存されます。

これは、mini_magickを使って画像からheightwidthを取り出すときによく使われます。

現在のActiveStorage::AnalysisJobdefaultキューに入ります。このフラグをコメント解除すると、専用のactive_storage_analysisキューに送信されるようになります。これによって、ジョブのカスタム優先順位レベルをアプリで設定できるようになります。

このフラグを安全に有効にするには

このフラグをコメント解除すると、新しいActiveStorage::AnalysisJobactive_storage_analysisキューに入るようになります。キューのバックエンドがこのキューのジョブを処理できるように設定されていることを確認する必要があります。

例: Sidekiqを使うアプリの場合、active_storage_analysisキューをconfig/sidekiq.ymlに追加する必要があります。ここに記載した順序によって、他のキューとの相対的な優先順位が決定されます。

既にキューに入った既存のジョブが壊れることはありません。従来どおりdefaultキューで処理されます。

6. active_storage.queues.purge = :active_storage_purge

フラグの機能

以下のコードがあるとします。

class User < ApplicationRecord
  has_one_attached :avatar
end

sam = User.create.avatar.attach(some_image_file)
sam.avatar.attach(different_image_file)

最初のファイルがアタッチされると、S3などのストレージにアップロードされ、Railsはそのストレージの場所を指すレコードを1件作成します。2番目のファイルがアタッチされると同様にS3にアップロードされますが、Railsはこのレコードを更新して、新しいストレージの場所を指すように変更します。

最初にS3にアップロードしたファイルはどうなったのでしょう?Active Storageはファイルをほったらかしにはしません。ActiveStorage::PurgeJobがキューに入り、最終的にファイルをS3から頑張って削除します。

現在のActiveStorage::PurgeJobdefaultキューに入ります。このフラグをコメント解除すると、以後は専用のactive_storage_purgeキューに送信されます。これによって、ジョブのカスタム優先順位レベルをアプリで設定できるようになります。

このフラグを安全に有効にするには

このフラグをコメント解除すると、新しいActiveStorage::PurgeJobactive_storage_purgeキューに入るようになります。キューのバックエンドがこのキューのジョブを処理できるように設定されていることを確認する必要があります。

例: Sidekiqを使うアプリの場合、active_storage_purgeキューをconfig/sidekiq.ymlに追加する必要があります。ここに記載した順序によって、他のキューとの相対的な優先順位が決定されます。

既にキューに入った既存のジョブが壊れることはありません。従来どおりdefaultキューで処理されます。

7. active_storage.replace_on_assign_to_many = true

フラグの機能

以下のコードがあるとします。

class Message < ApplicationRecord
  has_many_attached :uploads
end

files = get_array_of_files
message = Message.create(uploads: files)
files << get_another_file
message.update(uploads: files)

メッセージが作成されると、Active StorageはファイルをS3などのストレージにアップロードします。しかし、ここで最終行の配列uploadsに再代入したらどうなるでしょう?

  1. Railsは既存の配列と新しい配列の差を取る。新しいファイルをアップロードすると、配列に既に存在するファイルは無視される。
  2. Railsは配列の差分を取らない。既存のファイルをすべて破棄して新しい配列内のファイルをすべてアップロードする。

現在のRailsの挙動は1.です。コメント解除するとRailsの挙動は2.になります。

このフラグを安全に有効にするには

コードベースでhas_many_attachedをgrepします。これによって変更の影響を受けるモデルを特定できます。モデルが影響を受けないのであれば、このフラグを安全にコメント解除できます。

モデルが影響を受ける場合は、再代入を行っている箇所をリファクタリングしてattachを使うことをおすすめします。attachは、既存のファイルに触らずに、渡されたファイルを素朴にアタッチします。すなわち、ファイルの重複防止がアプリで重要な場合は、ファイル重複防止ロジックを自分で追加する必要があります。

8. action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"

フラグの機能

ActionMailer::DeliveryJobは、Railsメイラーでdeliver_laterが呼ばれたときにキューに入るジョブです。

RailsチームはActionMailer::DeliveryJobクラスにbreaking changeを入れることを望みました。しかしこれではRailsのアップグレード中に既存のジョブが壊れるかもしれません。そこで新たにActionMailer::MailDeliveryJobを作ることが決まりました。

このフラグをコメント解除すると、以後新しいActionMailer::MailDeliveryJobクラスによるメイラージョブがキューに入るようになります。

狙いは、新しいActionMailer::MailDeliveryJobがキューに入ってもActionMailer::DeliveryJobが引き続き処理されるようにすることです。ActionMailer::DeliveryJobのジョブは今後キューに入らなくなるので、このクラスは最終的に不要になります。実際、RailsチームはActionMailer::DeliveryJobをRails 6.1で削除する予定です。

このフラグを安全に有効にするには

The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
If you send mail in the background, job workers need to have a copy of
MailDeliveryJob to ensure all delivery jobs are processed properly.
Make sure your entire app is migrated and stable on 6.0 before using this setting.

大意: デフォルトのデリバリージョブ(ActionMailer::Parameterized::DeliveryJobActionMailer::DeliveryJob)はRails 6.1で削除されます。この設定は以前のRailsとの後方互換性がありません。メールをバックグラウンドで送信すると、ジョブワーカーはMailDeliveryJobですべてのデリバリージョブが正しく送信されるようにする必要があります。アプリの移行が完全に終わって6.0で安定運用するようになってから、この設定を使いましょう。

恐ろしげなwarningが表示されますが怖がることはありません。このフラグをいったんコメント解除すると、アプリがRails 6.xにロックインされますよと言っているのです。Rails 5.xにはActionMailer::MailerDeliveryJobクラスがないので、Rails 5.xへのダウングレードは不可能になります。

アプリがRails 6で安定運用できることを確認できれば、このフラグは安全にコメント解除できます。

9. active_record.collection_cache_versioning = true

フラグの機能

Rails 5.2でリサイクル可能なキャッシュキーが導入されました。この機能によって、Active Recordのcache_keyのうち変動の可能性があるupdated_atcache_versionに移動します。

# Rails 5.1の場合
user = User.last
user.cache_key # "users/281-20191007212244313194"
user.touch
user.cache_key # "users/281-20191017003012868191"
# Rails 5.2の場合
user = User.last
user.cache_key # "users/281"
user.cache_version # "20191007212244313194"
user.touch
user.cache_key # "users/281"
user.cache_version # "20191017003012868191"

上のコードで、Rails 5.1の場合とは異なり、Rails 5.2でキャッシュキーが変化しなくなっているのがわかります。キャッシュのミスヒットにつながるキーに頼るのではなく、Rails 5.2ではキーを再利用してキャッシュエントリを更新するようになりました。これによってキャッシュミスが減少し、パフォーマンスが向上します。

このフラグをコメント解除すると、リサイクル可能なキャッシュキーがActiveRecord::Relationなどのコレクションにも導入されます。

Rails.application.config.active_record.collection_cache_versioning = false
User.all.cache_key # "users/query-b03a3611aaa3ed0825f6b93870f69c0e-281-20191007212244313194"
Rails.application.config.active_record.collection_cache_versioning = true
User.all.cache_key # "users/query-b03a3611aaa3ed0825f6b93870f69c0e"

このフラグを安全に有効にするには

このフラグは安全にコメント解除できます。私は以下の2つの理由から、さほどややこしくならずに済むだろうと思うことにしています。

第1に、この変更が懸念材料となるのは、ユーザーのコードではなくキャッシュアダプタ(Redisなど)の側です。この機能をサポートしないモダンなアダプタはちょっと思いつきません。

第2に、既にBig Binaryの良記事(「recyclable cache keys」と「recyclable collection cache keys」)があることです。

10. config.autoloader = :zeitwerk

フラグの機能

これぞまさしくload_defaults 6.0有効にする第10のフラグです。しかしこれはnew_framework_defaultsには含まれていません。このフラグは、RailsのオートローダーをclassicからZeitwerkに切り替えます。

新しいデフォルト設定を読み込む前にZeitwerkを使いたいのであれば、application.rbでconfig.autoloader = :zeitwerkを呼べます。

このフラグを安全に有効にするには

Edgeガイドのマイグレーションガイドは読んでおく価値があります(参考: Railsガイド -- オートローディング)。しかし実際にはつらい点は1つしか見当たりません。Zeitwerkはイニシャライザ内でのオートロードをサポートしなくなりました。

テストを実行するときに以下のようなwarningが出力されていないかどうか探してみましょう。

DEPRECATION WARNING: Initialization autoloaded the constant Foo.

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 Foo, for example,
the expected changes won’t be reflected in that stale Class object.

These autoloaded constants have been unloaded.

大意: 非推奨警告: 初期化時に定数Fooがオートロードされました。
この挙動を有効にする機能は非推奨であり、初期化時のオートロードは今後のバージョンのRailsでエラーになります。
再読み込みしてもアプリは再起動しないので、コードは初期化中に再実行されません。したがってFooを再読み込みしても、期待する変更は古いClassオブジェクトに反映されなくなります。
オートロードされたこれらの定数はアンロードされました。

何が問題か

以下の2つのファイルがあるとします。

# app/foo.rb
class Foo
  def self.bar?
    true
  end
end
# config/initializers/baz.rb
BAZ = Foo

classicモードのRailsは、イニシャライザで上のコードがある場合にFooを問題なくオートロードします。BAZ.bar?は期待どおりtrueを返します。

foo.rbを編集してfalseを返すようにしたらどうなるでしょうか。BAZ.bar?はtrueを返します。理由は、コードが再読み込みされてもイニシャライザは再実行されないからです。Fooの値はアプリケーションを再起動するまでそのままになります。

Zeitwerkでは、そのような自分の足を撃ち抜く銃を使えません。初期化中にオートロードを検出すると、定数をアンロードしてwarningを表示します。

解決法

そのオートローディングを削除することです。Fooが1つのイニシャライザだけで参照されているのであれば、定義をそのイニシャライザに移動するだけで済みます。

# config/initializers/baz.rb
class Foo
  def self.bar?
    true
  end
end
BAZ = Foo

複数箇所から参照されているのであれば、オートロードされない場所(libなど)にfoo.rbを移動し、明示的にrequireします。

# config/initializers/baz.rb
require Rails.root.join('lib', 'foo')
BAZ = Foo

warningが消えてテストがgreenになれば、Zeitwekを安心して使えるようになります。

おたより発掘

関連記事

Rails 6 Beta2時点のZeitwerk情報(要訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。