- Ruby / Rails関連
週刊Railsウォッチ: Active Storageバリアントの事前変換、Linkヘッダープリロードのオプトアウトほか(20230802前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Active Storageのバリアントをバックグラウンドで事前変換するオプションが追加
Active Storageのバリアントは必要が生じると即座に処理されるが、それらにアクセスされることが確実な場合は事前に処理したいときがある。
バリアントを宣言するときの
preprocessed
オプションが追加された。class User < ApplicationRecord has_one_attached :avatar do |attachable| attachable.variant :thumb, resize_to_limit: [100, 100], preprocessed: true end end
Shouichi Kamiya
同Changelogより
参考: § 9 画像を変形する -- Active Storage の概要 - Railsガイド
つっつきボイス:「バリアント画像は参照されたときに処理されるけど、それを事前にできるオプションも欲しいので追加したということか: shrineやcarrierwaveにも同じ機能があった気がする」「preprocessed: true
を指定すると事前処理するんですね」「バックグラウンドで処理するためにActiveStorage::TransformJob
でジョブを追加するようになった↓、なるほど」「これは必要な機能👍」
# activestorage/app/jobs/active_storage/transform_job.rb
# frozen_string_literal: true
class ActiveStorage::TransformJob < ActiveStorage::BaseJob
queue_as { ActiveStorage.queues[:transform] }
discard_on ActiveRecord::RecordNotFound
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
def perform(blob, transformations)
blob.variant(transformations).process
end
end
なお、#47473には追加修正も入っていました↓
🔗 Rails 7.1のAction TextのサニタイザでRails::HTML5::SafeListSanitizer
をデフォルトで使うようになった
Rails 7.1コンフィグで
Rails::HTML5::SafeListSanitizer
をデフォルトで使うようになった(サポートされている場合)。Action Textのサニタイザは
config.action_text.sanitizer_vendor
で設定可能。サポートされる値はRails::HTML4::Sanitizer
またはRails::HTML5::Sanitizer
。Rails 7.1の設定では、これは
Rails::HTML5::Sanitizer
に設定され(サポートされている場合)、そしてRails::HTML4::Sanitizer
にフォールバックする。
以前の設定ではデフォルトでRails::HTML4::Sanitizer
だった。Mike Dalessio
同Changelogより
#48523は、Action Textの動作を変更し、利用可能な場合にはHTML5の
SafeListSanitizer
を使い、それ以外の場合にはHTML4のSafeListSanitizer
にフォールバックするようにした。
@rafaelfrancaの提案に基づき、このプルリクではconfig.action_text.sanitizer_vendor
(config.action_view.sanitizer_vendor
と非常に似ている)とRails 7.1のデフォルトを導入し、その振る舞いの変更を制御する。このプルリクは、
config.action_view.sanitizer_vendor
のテストカバレッジも補完する。詳細
このプルリクは、config.action_text.sanitizer_vendor
を導入する。これは、config.action_view.sanitizer_vendor
と同じように振る舞う。追加情報
Railsのメンテナーが、Action ViewとAction Textに本当に2つの設定パラメータが必要と考えているのか、それともRails全体で
config.sanitizer_vendor
のようなものを統合すべきと考えているのかについて興味がある。
同PRより
つっつきボイス:「最近HTML5(正式名はHTML Living Standard)関連の改修が増えていますけど(ウォッチ20230621、ウォッチ20230719)、これもそうなんですね」「HTML4のライブラリとHTML5のライブラリを混ぜて使うと不整合になってしまうので仕方ないですね」
参考: Action Text の概要 - Railsガイド
🔗 ActiveSupport::JSON.encode
を高速化
- PR: Improve performance of ActiveSupport::JSON.encode by jhawthorn · Pull Request #48614 · rails/rails
これにより、
.to_json
/ActiveSupport::JSON.encode
のパフォーマンスがほぼ倍増する(データがすでにJSON対応であり、オプションを指定しない場合に最も影響が大きいが、すべてのケースで高速化されるはず)。この変更はユーザーにとってほぼ透明である。わかりやすくするために、この変更を3つのコミットに分割した。1:
ActiveSupport::JSON.dump
のオプションを使わないことで余分な処理を回避する従来の
JSONGemEncoder.encode
は、常に2パスで実行していた。
最初に.as_json(options)
を呼び出し、その後は再帰的に.as_json
(今度はオプションなしで)を呼び出して、データが「JSON-ready」表現に収束するまで、パス2のjsonify
を実行していた。オプションが指定されていない場合、パス2はパス1と同等になるはずなので、それを検出して「jsonify」ステップのみを実行する。
この変更でユーザーから唯一見える効果は、空のハッシュを渡すのではなく
as_json
にオプションを渡さないようにすることだが、既存のas_json
の実装はそれを受け入れることが期待されている(著者編集:ただし、Active ModelとActive Recordでそれぞれ1つのテストケースが壊れた)。これは、数年前に@tenderloveと@eileencodesが行った#34633の「軽量版」と考えてもよいかもしれない(これは依然として素晴らしいアイデアであり、将来的な実装になる可能性もあるが、このプルリクよりも変更が広範囲になる)。
Railsは、JSON gemと比べて文字列を追加でエスケープしている(
U+2028
、U+2029
、<
、>
および&
もエスケープする)。JSONでは、これらの文字が有効な場所はJSON文字列内だけなので、出力時にエスケープ処理を行っても同等であり、かつ高速になる。このコミットは最もパフォーマンスに影響を与えるため、入力と無関係に
.to_json
の呼び出しを強化できる。このエスケープ処理はまだかなりコストが高いが、エスケープルールの調整や上流でより高速な実装を検討していく予定。3: シンボルを"JSON-ready"とみなして
jsonify
を改善従来の
jsonify
は、Integer
、nil
、true
、false
に対しても.as_json
を呼び出していたが(これは#26933で導入されたらしい)、これらの型は「JSON-ready」と見なされている。技術的にはユーザーがこれらの型の.as_json
をオーバーライドする可能性も一応あるが、このユースケースは想像できないし、サポートすべきではないと考えている。
Numeric
のジェネリックな「.as_json」呼び出しの挙動は変更しなかった(ユーザーがサブクラスを持っている場合にas_json
が実装されている可能性があるため)。この挙動はFloat
でも使われる(NaN
/Infinity
/-Infinity
をnil
に変換する)。これにより、「JSON-ready」タイプのリストにシンボルも追加されるようになって入力時のエスケープ処理が行われなくなるので、文字列への不要なキャストを回避できる。
jsonify
の出力はJSON.generate
を通る前にユーザーから見えることはないため、ユーザーには見えないだろう。これにより、Hashの処理も修正されてすべてのキーに対して
to_s
を呼び出すようになる。これは.as_json
の振る舞いとJSONの要件(キーはStrings
でなければならない、JSON gemはシンボルを文字列に変換することを認識している)と一致する。
同PRより
つっつきボイス:「詳しくは見てないけど、危険な文字が通過しない範囲で、しなくてもいい処理をスキップしたり、エスケープのタイミングを変更したり、シンボルをJSON-readyと見なしたりすることで高速化したようですね」
🔗 Link
ヘッダーのプリロードをオンオフできるようになった
stylesheet_link_tag
やjavascript_include_tag
の呼び出しでLink preload
ヘッダーのオプトイン/アウトを可能になった。# 設定が有効であってもヘッダーを除外する javascript_include_tag("http://example.com/all.js", preload_links_header: false) # 設定が無効であってもヘッダーをインクルードする stylesheet_link_tag("http://example.com/all.js", preload_links_header: true)
Alex Ghiculescu
同Changelogより
参考: §1.1.8 stylesheet_link_tag
-- Action View ヘルパー - Railsガイド
参考: §1.1.5 javascript_include_tag
-- Action View ヘルパー - Railsガイド
つっつきボイス:「今まではアセットの読み込みでLink
ヘッダーのプリロードがデフォルトで有効になっていたけど、preload_links_header:
オプションでプリロードするかどうかを指定できるようになったのか」「issue #48517によると、プリロードをオフにしないとimportmapが壊れることがあったみたいですね」「今もconfig.action_view.preload_links_header
でグローバルに変更はできるけど、stylesheet_link_tag
やjavascript_include_tag
のオプションとして指定できるようにしたのね」
参考: §3.11.17 config.action_view.preload_links_header
-- Rails アプリケーションを設定する - Railsガイド
「Link
ヘッダーってどれかと思ったらこれのことか↓」「これ知りませんでした」
「Link
ヘッダーの関連情報に103 Early Hintsステータスへのリンクもある: たとえばレスポンスのボディはレンダリングが終わってから出力する必要があるような場合に、レスポンスでLink
ヘッダーを先に返すことで、レンダリング中にリソースをブラウザ側でプリロードできるようになるんですね」「なるほど」「プロキシがらみなどでこのプリロードをオプトアウトしたくなる状況はありそう」
HTTP 103 Early Hints
インフォメーションレスポンスステータスコードは、主に Link ヘッダーと共に使用され、サーバーがまだレスポンスを準備している間にユーザーエージェントがリソースのプリロードを開始できるようにすることを目的としています。
103 Early Hints - HTTP | MDNより
🔗 クエリログが有効な場合はプリペアドステートメントを無効にするよう修正
修正: #48398
プリペアドステートメントとクエリログは互換性がない。クエリログはすべてのクエリを一意にするため、プリペアドステートメントとは組み合わせられない。
Marginalia gemではこの点をいい感じに説明している。
Marginaliaでプリペアドステートメントを使う際は注意すること。
request_id
などのコンポーネントを使うとすべてのクエリが一意になるため、Active Recordは新しいプリペアドステートメントをクエリごとに作成するようになり、それが元でシステムリソースが枯渇する可能性がある。cardinalityが大きいコンポーネントを使いたい場合は、プリペアドステートメントを無効にすること。
同PRより
つっつきボイス:「Railsではデフォルトでプリペアドステートメントを有効にしたログが出力されるけど、クエリログを有効にするとプリペアドステートメントなしのログが出力されるということか: このあたりは知っておくというか気をつけておいてもよさそう」
参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.5 プリペアドステートメント
「プリペアドステートメントを使わないとどうなるんでしょうか?」「値も含めて生のSQLクエリログがすべて出力されるようになりますね: そうなるとDB側のクエリオプティマイザのキャッシュが効きにくくなるなどのデメリットはあると思います」「なるほど」
参考: プリペアドステートメントとは - 意味をわかりやすく - IT用語辞典 e-Words
🔗 belongs_to :inverse_of
を介したhas_one :through
関連付けで、レコードをビルドできるように修正
- PR: Fix has_one through singular building with inverse. by gmcgibbon · Pull Request #48674 · rails/rails
has_one through
の単一の逆関連付けを介して、レコードのビルドが可能になった(belongs_to through
の場合は外部キーと主キーモデルのリンクは不要)。
has_one
では関連付けをミュータブルにできないため、レコードをビルドできない。
Gannon McGibbon
同Changelogより
動機/背景
このプルリクを作成した理由は、has_one
through:
の振る舞いが壊れていて、ビルドしたレコードにリンクしようとするとクラッシュするため。詳細
このプルリクは、
ActiveRecord::Associations::ThroughAssociation
を変更して、ソースのリフレクションが単一(has_one
/belongs_to
)の場合は逆方向へのリンクを持つビルドされたレコードをスキップするようにした。
このコードはコレクションの関連付けのみを対象とする。追加情報
ついでに、record.build_<関連付け名>
がデフォルトでは属性ハッシュを含んでいないことにもデバッグ中に気づいた(1d4c288)。def build_#{name}(*args, &block)
しかし
record.<関連付け名>s.build
は以下のようになる(1d4c288)。def build(attributes = {}, &block)
これはバグかもしれないが、単一の関連付けコードではこれを考慮しているらしいので、修正は不要だった。
ActiveRecord::Associations::ThroughAssociation#build_record
が唯一のコードパスらしい。ここではattributes
がハッシュであると仮定している。
同PRより
つっつきボイス:「has_one through:
って使ったことなかったかも」「こういうinverseがらみの修正が前にもあった気がしますね」
参考: 2.5 has_one :through
関連付け -- Active Record の関連付け - Railsガイド
# activerecord/lib/active_record/associations/through_association.rb#L117
def build_record(attributes)
- inverse = source_reflection.inverse_of
- target = through_association.target-
- if inverse && target && !target.is_a?(Array)
- Array(target.id).zip(Array(inverse.foreign_key)).map do |primary_key_value, foreign_key_column|
- attributes[foreign_key_column] = primary_key_value
+ if source_reflection.collection?
+ inverse = source_reflection.inverse_of
+ target = through_association.target
+
+ if inverse && target && !target.is_a?(Array)
+ Array(target.id).zip(Array(inverse.foreign_key)).map do |primary_key_value, foreign_key_column|
+ attributes[foreign_key_column] = primary_key_value
+ end
end
end
プルリクメッセージだけだとわかりにくいので、テストコードを部分的に引用しました。
# https://github.com/gmcgibbon/rails/blob/hmt_singular_fix/activerecord/test/models/member.rb
class Member < ActiveRecord::Base
has_one :current_membership
# ...
has_one :club, through: :current_membership
# https://github.com/gmcgibbon/rails/blob/hmt_singular_fix/activerecord/test/models/membership.rb
class CurrentMembership < Membership
belongs_to :member
belongs_to :club, inverse_of: :membership
end
# https://github.com/gmcgibbon/rails/blob/hmt_singular_fix/activerecord/test/cases/associations/has_one_through_associations_test.rb#L103
def test_building_works_with_has_one_through_belongs_to
new_member = Member.create!(name: "Joe")
new_member.create_current_membership!
new_club = new_member.build_club
assert_equal(new_member.club, new_club)
end
🔗 ネストしたfield_id
とfield_name
の添字の値を二重エンコードしないようになった
field_id
とfield_name
のビューヘルパーメソッドには、デフォルトのキーワード引数としてindex: @options
を渡すこと。
Sean Doyle
同PRより
つっつきボイス:「field_id
とfield_name
はAction Viewのメソッドですね」「この[0]
が余分に追加されていたのか↓」
field_name
は、余分な添字パラメータ[0]
をparent[children_attributes][0][0][grandchildren_attributes][]
のように追加しています。
#47436より
# actionview/lib/action_view/helpers/form_helper.rb#L1770
- def field_id(method, *suffixes, namespace: @options[:namespace], index: @index)
+ def field_id(method, *suffixes, namespace: @options[:namespace], index: @options[:index])
@template.field_id(@object_name, method, *suffixes, namespace: namespace, index: index)
end
# actionview/lib/action_view/helpers/form_helper.rb#L1770
- def field_name(method, *methods, multiple: false, index: @index)
+ def field_name(method, *methods, multiple: false, index: @options[:index])
object_name = @options.fetch(:as) { @object_name }
@template.field_name(object_name, method, *methods, index: index, multiple: multiple)
end
前編は以上です。
バックナンバー(2023年度第3四半期)
- 20230725前編 config.autoload_libとconfig.autoload_lib_onceが追加ほか
- 20230721後編 Kaigi on Rails 2023プロポーザル募集、rubocop-magic_numbersほか
- 20230719前編 複合主キー関連の実装進む、Action TextでHTML5サニタイザほか
- 20230705後編 AWS LambdaでRailsをRackで動かすLambyほか
- 20230704前編 productionのforce_ssl=trueがデフォルトで有効に、rakeタスクをthorで書くほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)