- 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 endShouichi 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_onethrough:の振る舞いが壊れていて、ビルドしたレコードにリンクしようとするとクラッシュするため。詳細
このプルリクは、
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ウォッチタグ)