概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Rails 6.0 new framework defaults: what they do and how to safely uncomment them
- 原文公開日: 2019/10/19
- 著者: Dylan Reile -- ES6やSQL経験豊富な情熱的Rubyist
Rails 6の新しいデフォルト設定と安全な移行方法を詳しく解説(翻訳)
本記事では、rails app:update
で生成されるnew_framework_defaults_6_0.rb
の9つのデフォルトフラグについてひととおり解説します。最後までお読みいただければ、自信を持ってこのファイルを削除し、application.rb
にload_defaults 6.0
と堂々と書けるようになります。
本記事ではRails 5.2のデフォルト設定を使っているアプリを前提としています。これについては、自分のアプリのapplication.rb
にload_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フォームのhiddenフィールドに居座っています。このフラグのコメントを外すと、チェックマーク記号はフォームに含まれなくなります。
このフラグを安全に有効にするには
accept-charset
のすっとこどっこいな振る舞いはIE 5からIE 8まで続きました。これらのブラウザをサポートしなくてもよければ、コメントを外しても安全です。
⚓2. action_dispatch.use_cookies_with_metadata = true
フラグの機能
Railsはcookie値を暗号化および署名し、cookie値の暗号化を解除して署名を照合することで、悪者によって改変されていないことを確認します。しかしこの方法では、一部のcookieで暗号化済みおよび署名済みの値を悪者がコピーして、他のcookieの値として用いることまでは防げません。
次のシナリオで考えてみましょう。
- Railsに次の2つのcookieが設定されているとする
is_admin
:false
is_a_doofus
:true
(doofus: アホ)
- 2つのcookieの暗号化/署名済みの値を悪者が入れ替える
- Railsはリクエストに含まれる値を読み取って「よろしい、値は改変されていない」と承認する
- 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
がスローされてjob2
はfalse
になります。
このフラグを安全に有効にするには
コードベースでabort
をgrepしてください。abort
のインスタンスを調べて、ジョブのコールバックにabort
が出現しないようにしましょう。
ジョブのコールバックでabort
を見つけてしまったら、ジョブがどこでキューに入ったかを調べる必要があります。常にジョブインスタンスにするために、その箇所がperform_later
に依存しないようにします。運悪くperform_later
に依存していたら、false
でもやっていけるようにリファクタリングする必要があります。
⚓5. active_storage.queues.analysis = :active_storage_analysis
フラグの機能
Active Storageでファイルがアタッチされると、ActiveStorage::Attachment
のafter_commit_create
コールバックが呼び出されます。このコールバックによってActiveStorage::AnalysisJob
がキューに入ります。そして実行されると、このジョブでActiveStorage::Blob#analyze
が呼び出されます。このメソッドは、ファイルからメタデータを抽出するのにプラグインシステムを用いています。このメタデータは、blobレコードのmetadata
カラムに保存されます。
これは、mini_magick
を使って画像からheight
やwidth
を取り出すときによく使われます。
現在のActiveStorage::AnalysisJob
はdefault
キューに入ります。このフラグをコメント解除すると、専用のactive_storage_analysis
キューに送信されるようになります。これによって、ジョブのカスタム優先順位レベルをアプリで設定できるようになります。
このフラグを安全に有効にするには
このフラグをコメント解除すると、新しいActiveStorage::AnalysisJob
はactive_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::PurgeJob
はdefault
キューに入ります。このフラグをコメント解除すると、以後は専用のactive_storage_purge
キューに送信されます。これによって、ジョブのカスタム優先順位レベルをアプリで設定できるようになります。
このフラグを安全に有効にするには
このフラグをコメント解除すると、新しいActiveStorage::PurgeJob
はactive_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
に再代入したらどうなるでしょう?
- Railsは既存の配列と新しい配列の差を取る。新しいファイルをアップロードすると、配列に既に存在するファイルは無視される。
- 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::DeliveryJob
とActionMailer::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_at
がcache_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を安心して使えるようになります。
おたより発掘
神便利記事きたー!! / 1件のコメント https://t.co/7m9YR1CiPc “Rails 6の新しいデフォルト設定と安全な移行方法を詳しく解説(翻訳)” https://t.co/vUfwe4YkLC
— igaiga (@igaiga555) November 15, 2019