- Ruby / Rails関連
週刊Railsウォッチ: MessagePackがcookieシリアライザとメッセージシリアライザにも導入ほか(20230607前編)
こんにちは、hachi8833です。Rails World 2023のCFP締め切りが近づいています。
Friendly reminder that the #RailsWorld #CFP will close on Friday June 16 - that means less than two weeks to submit your talk proposals here➡️ https://t.co/Tm165uON8x
— Ruby on Rails (@rails) June 5, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 config.filter_parameters
に同じフィルタが再追加されないよう修正
Active Recordの暗号化属性が宣言されると、そのためのフィルタが自動的に
config.filter_parameters
に追加される。このコミット以前は、モデルが再読み込みされるたびにフィルターが再追加されていた:class Post < ActiveRecord::Base encrypts :title end
irb> Rails.application.config.filter_parameters # => [:passw, ..., :ssn] irb> Post irb> Rails.application.config.filter_parameters # => [:passw, ..., :ssn, "post.title"] irb> reload! irb> Post irb> Rails.application.config.filter_parameters # => [:passw, ..., :ssn, "post.title", "post.title"]
このコミットによって、フィルタが1回だけ追加されるようになり、
config.filter_parameters
が際限なく大きくならないようになる。
同PRより
つっつきボイス:「config.filter_parameters
に同じフィルタが複数追加されても動くけど、たしかに動作としてはよろしくないですね」「修正はinclude?(filter)
を追加したシンプルなものですね」
# activerecord/lib/active_record/encryption/configurable.rb#L51
- def install_auto_filtered_parameters_hook(application) # :nodoc:
+ def install_auto_filtered_parameters_hook(app) # :nodoc:
ActiveRecord::Encryption.on_encrypted_attribute_declared do |klass, encrypted_attribute_name|
- filter_parameter = [("#{klass.model_name.element}" if klass.name), encrypted_attribute_name.to_s].compact.join(".")
- unless excluded_from_filter_parameters?(filter_parameter)
- application.config.filter_parameters << filter_parameter
+ filter = [("#{klass.model_name.element}" if klass.name), encrypted_attribute_name.to_s].compact.join(".")
+ unless excluded_from_filter_parameters?(filter)
+ app.config.filter_parameters << filter unless app.config.filter_parameters.include?(filter)
klass.filter_attributes += [encrypted_attribute_name]
end
end
end
参考: §3.2.21 config.filter_parameters
-- Rails アプリケーションを設定する - Railsガイド
🔗 匿名でないモジュールをfreeze
しないよう修正
#48106で、モジュールが匿名でない場合に
Module#deep_dup
が(コピーではなく)モジュールそのものを返すように変更された。しかしこれによって、匿名でないモジュールがActiveModel::Type::Helpers::Mutable#immutable_value
に渡されるとvalue.deep_dup.freeze
がフリーズするため、たとえば、クラス属性をモジュールに設定できなくなってしまう。こうした問題を防ぐために、このコミットで
immutable_value
のfreeze
を削除した。immutable_value
はActiveRecord::PredicateBuilder#build_bind_attribute
からのみ呼び出されるメソッドで、その目的は他のコードが値を改変できないように保護することだけであり、その値を実際にfreeze
することではない。
これによって、Rails #96281のテストなどの独立したテストの失敗が修正される。
同PRより
つっつきボイス:「メソッド名はimmutable_value
だけど、dup
して返すものをさらにfreeze
するとクラス属性とかを設定できなくなるのでdup
するだけでいい」「修正はfreeze
を外しただけなんですね」
# activemodel/lib/active_model/type/helpers/mutable.rb#L5
module Helpers # :nodoc: all
module Mutable
def immutable_value(value)
- value.deep_dup.freeze
+ value.deep_dup
end
🔗 schema.table.column
のようにカラム指定でスキーマも指定可能になった
動機/背景
ActiveRecord::PredicateBuilder
は現在、カラム名をピリオド1つのドット表記で指定することのみを想定している。スキーマに名前空間があるテーブルに対してドット表記でカラムを指定すると、スキーマ名がテーブル名として使われる。このプルリクでは、table.column
だけでなく、schema.table.column
というフォーマットでもカラムを指定可能になる。
修正されるissue: #48172
詳細
このプルリクは、ActiveRecord::PredicateBuilder.references
やActiveRecord::PredicateBuilder#convert_dot_notation_to_hash
をの振る舞いを変更する。
CHANGELOGには記載しなかったが、この変更をレビュアーが重要視するのであれば喜んで追記する。
同PRより
つっつきボイス:「なるほど、PostgreSQLとかにあるschema.table.column
のような書き方をサポートしたんですね👍」「今まではtable.column
みたいにドットを1つしか書けなかったのか」「今まではセッションで指定しているデフォルトのスキーマからしか取れなかったということですね」
# activerecord/test/cases/relation/predicate_builder_test.rb#L26
+ def test_references_with_schema
+ assert_equal %w{schema.table}, ActiveRecord::PredicateBuilder.references(%w{schema.table.column})
+ end
+
+ def test_build_from_hash_with_schema
+ assert_match %r{schema.+table.+column}i, Topic.predicate_builder.build_from_hash("schema.table.column" => "value").first.to_sql
+ end
+
+ def test_does_not_mutate
+ defaults = { topics: { title: "rails" }, "topics.approved" => true }
+ Topic.where(defaults).to_sql
+ assert_equal({ topics: { title: "rails" }, "topics.approved" => true }, defaults)
+ end
🔗 MessagePack関連
🔗 cookieシリアライザとメッセージシリアライザでも:message_pack
形式をサポート
- PR: Support
:message_pack
as cookies serializer by jonathanhefner · Pull Request #48103 · rails/rails
config.action_dispatch.cookies_serializer
に:message_pack
や:message_pack_allow_marshal
をシリアライザとして渡せるようになった。これらのシリアライザではmsgpack
gem (>= 1.7.0)が必要。MessagePack形式は、パフォーマンスの向上とペイロードサイズの縮小を実現できる。また以下のように、JSONでサポートされていない一部のRuby型のラウンドトリップもサポートする。
cookies.encrypted[:foo] = [{ a: 1 }, { b: 2 }.with_indifferent_access, 1.to_d, Time.at(0, 123)] # 変更前: config.action_dispatch.cookies_serializer = :json cookies.encrypted[:foo] # => [{"a"=>1}, {"b"=>2}, "1.0", "1969-12-31T18:00:00.000-06:00"] cookies.encrypted[:foo].map(&:class) # => [Hash, Hash, String, String] # 変更後:config.action_dispatch.cookies_serializer = :message_pack cookies.encrypted[:foo] # => [{:a=>1}, {"b"=>2}, 0.1e1, 1969-12-31 18:00:00.000123 -0600] cookies.encrypted[:foo].map(&:class) # => [Hash, ActiveSupport::HashWithIndifferentAccess, BigDecimal, Time]
:message_pack
シリアライザは必要に応じてActiveSupport::JSON
でデシリアライズする形にフォールバックし、:message_pack_allow_marshal
シリアライザはActiveSupport::JSON
に加えてMarshal
でもデシリアライズする形にフォールバックする。
さらに、:marshal
、:json
、:json_allow_marshal
(:hybrid
とも呼ばれる)シリアライザも必要に応じてActiveSupport::MessagePack
でデシリアライズする形にフォールバックする。この振る舞いによって古いcookieも引き続き読めるようになり、移行しやすくなる。
Jonathan Hefner
同Changelogより
- PR: Support
:message_pack
as message serializer by jonathanhefner · Pull Request #47964 · rails/rails
MessageEncryptor
、MessageVerifier
、config.active_support.message_serializer
に:message_pack
や:message_pack_allow_marshal
をシリアライザとして渡せるようになった。これらのシリアライザではmsgpack
gem (>= 1.7.0)が必要。MessagePack形式は、パフォーマンスの向上とペイロードサイズの縮小を実現できる。また以下のように、JSONでサポートされていない一部のRuby型のラウンドトリップもサポートする。
verifier = ActiveSupport::MessageVerifier.new("secret")
data = [{ a: 1 }, { b: 2 }.with_indifferent_access, 1.to_d, Time.at(0, 123)]
message = verifier.generate(data)
# 変更前: config.active_support.message_serializer = :json
verifier.verified(message)
# => [{"a"=>1}, {"b"=>2}, "1.0", "1969-12-31T18:00:00.000-06:00"]
verifier.verified(message).map(&:class)
# => [Hash, Hash, String, String]
# 変更後: config.active_support.message_serializer = :message_pack
verifier.verified(message)
# => [{:a=>1}, {"b"=>2}, 0.1e1, 1969-12-31 18:00:00.000123 -0600]
verifier.verified(message).map(&:class)
# => [Hash, ActiveSupport::HashWithIndifferentAccess, BigDecimal, Time]
:message_pack
シリアライザは必要に応じてActiveSupport::JSON
でデシリアライズする形にフォールバックし、:message_pack_allow_marshal
シリアライザはActiveSupport::JSON
に加えてMarshal
でもデシリアライズする形にフォールバックする。
さらに、:marshal
、:json
、:json_allow_marshal
(:hybrid
とも呼ばれる)シリアライザも必要に応じてActiveSupport::MessagePack
でデシリアライズする形にフォールバックする。この振る舞いによって古いメッセージも引き続き読めるようになり、移行しやすくなる。
Jonathan Hefner
同Changelogより
つっつきボイス:「最近Active SupportとキャッシュシリアライザでMessagePackがサポートされましたけど(ウォッチ20230502)(ウォッチ20230530)、今度はcookieシリアライザとメッセージシリアライザでもMessagePackがサポートされました」「2つのプルリクメッセージは主語以外ほとんど同じ内容ですね」
「今後Rails内部の各種シリアライザでは、cookieのようにユーザー側で保持するものも含めて基本的にMessagePackを使っていこうという動きですね: MessagePackはバイナリフォーマットなので文字列を介するような変換が途中で発生したりするとオーバーヘッドが出るかもしれないけど、メモリ上でやりとりする分にはいいと思います👍」「JSON
やMarshal
による古いフォーマットへのフォールバックについても配慮されているので移行しやすそう」
参考: MessagePack: JSONをもっと速く、小さく。
🔗 メッセージシリアライザのデフォルトが:json_allow_marshal
になった
このコミットより前は、
config.load_defaults 7.1
を指定すると古いメッセージ(または古いアプリのメッセージ)が読めなくなってしまうことがあり、開発者はこれを防ぐために手動でconfig.active_support.message_serializer = :json_allow_marshal
を設定することが推奨されていた。このコミットによって、
config.load_defaults 7.1
で設定されるデフォルトのメッセージシリアライザが:json
から:json_allow_marshal
に変更され、アップグレードしたアプリで設定を追加せずに古いメッセージを引き続き読めるようになる。最終的な意図は、JSONでシリアライズされたメッセージの生成をアプリでしばらく続けた後、Rails 7.2でデフォルトを(
Marshal
にフォールバックしない):json
に変更することである。Rails 7.2より前のアプリは、手動で
config.active_support.message_serializer = :json
を設定することでJSONのみのシリアライズを選択できる。
修正: #48118
同PRより
つっつきボイス:「メッセージシリアライザがJSON
やMarshal
フォーマットにフォールバックするようにしたうえで、従来デフォルトだった:json
が:json_allow_marshal
に変わったのか: こうやって内部データ形式の変更にちゃんとフォールバック手段が用意されたことで段階的移行がやりやすくなるのはいい👍」「自力で同じことをやろうとするとテストとかが結構大変になりますよね」
参考: Switch ActiveSupport::MessageEncryptor
Default Serializer to JSON by fresh-eggs · Pull Request #42846 · rails/rails
参考: Rails API ActiveRecord::Encryption::MessageSerializer
🔗 キャッシュキーに空リストを渡した場合に早期脱出するようになった
修正: #48145
read_multi
、write_multi
、fetch_multi
は、空リストを渡して呼び出されたら早期脱出すべき。
実際に修正すべきは、空のキーリストを正しく扱えるようにすることだと信じている。
cc @joshuay03: そちらのAction Viewテストについて共著扱いにする。
同じくcc @jonathanhefner
同PRより
つっつきボイス:「これもシンプルな修正」「キャッシュキーが空なら読み出しや書き込みなどは不要なので早期脱出する方がいいですね」
# activesupport/lib/active_support/cache.rb#L439
def read_multi(*names)
+ return {} if names.empty?
+
options = names.extract_options!
options = merged_options(options)
instrument :read_multi, names, options do |payload|
read_multi_entries(names, **options, event: payload).tap do |results|
payload[:hits] = results.keys
end
end
end
参考: Rails API ActiveSupport::Cache::Store
🔗 Active Recordのカウンタインクリメントやデクリメントに1以外の増分量を効果的に指定できるようになった
ユーザーが(たとえば何かデータをインポートした後で)何らかのカウンタを1以外の増分量(amount)でインクリメントしたい場合、以下のようにする必要がある。
increment_counter
を数回呼び出す: これは遅くなる。where(id: id).update_all("counter = counter + 5")
を呼び出す: これはincrement_counter
よりも不便(COALESCEを使う場合や、楽観的ロックのカラムをインクリメントする場合は、それらにも気を遣うことになる)。record.increment(:counter, 5)
を呼び出す: カウンタをメモリ上で更新してから値をデータベースに書き込むが、未更新の値が競合状態によって上書きされる可能性がある。利用例:
Company.increment_counter(:users_count, 5, by: 3)
この変更は、Railsの既存のインクリメントメソッド
ActiveRecord::Persistance#increment
およびActiveSupport::CacheStore#increment
と同等(どちらも既に増分量を受け取れる)。
つっつきボイス:「カウンタをインクリメント・デクリメントするときに1以外の差分値を指定したいというのはよくあるテーマですね: これをやりたいのはわかる」
「ところでプルリクメッセージにあった2種類の既存increment
やdecrement
↓を見てみると、増分量を指定する引数名が違うのが気になりますね」「ほんとだ、Active Recordのはby
だけどActive Supportのはamount
になってる」「英語的にはby
の方が自然な気はするかな」「最初amount
という引数名を見たときにてっきりカウンタの上限値のことかと思っちゃいました」
参考: Rails API increment
-- ActiveRecord::Persistence
参考: Rails API increment
-- ActiveSupport::Cache::Store
参考: PostgreSQL 15.0ドキュメント 9.18.2. COALESCE
-- 9.18. 条件式
🔗 保持するクエリキャッシュを直近50件までに変更した
実際にどの程度なのかはわからないが、実行に時間のかかるジョブを処理中にクエリキャッシュによってメモリが枯渇してしまう問題を何度も耳にしている。
総じて、これを際限なくキャッシュしないようにするのが賢明に思える。以下のフィードバックを受けてこのプルリクをオープンした。
cc @tenderlove: もう9ce0211の文脈を覚えてないだろうけど、自分は基本的にこれを取り消すつもりなのでメンションしておく。
ネットで見かけた不満:
Active RecordのクエリキャッシュでLRU(Least Recently Used)エントリを追い出すようにした。
デフォルトでは、直近用いられた
50
件のクエリだけを維持する。
キャッシュサイズはdatabase.yml
で設定可能。development: adapter: mysql2 query_cache: 100
クエリキャッシュを完全に無効にすることも可能。
development: adapter: mysql2 query_cache: false
Jean Boussier
同Changelogより
つっつきボイス:「クエリキャッシュの枯渇ってあまり気にしたことなかったけど、あるんですね」「LRUはこういうときによく使われるアルゴリズムですね」
参考: Least Recently Used - Wikipedia
「ここではキャッシュヒットしたかどうかをActiveSupport::Notifications
を使って調べられるようにしてあるんですね」
# activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb#L118
private
def lookup_sql_cache(sql, name, binds)
+ key = binds.empty? ? sql : [sql, binds]
+ hit = false
+ result = nil
+
@lock.synchronize do
- if @query_cache[sql].key?(binds)
- ActiveSupport::Notifications.instrument(
- "sql.active_record",
- cache_notification_info(sql, name, binds)
- )
+ @query_cache[sql][binds]
+ if (result = @query_cache.delete(key))
+ hit = true
+ @query_cache[key] = result
end
end
+
+ if hit
+ ActiveSupport::Notifications.instrument(
+ "sql.active_record",
+ cache_notification_info(sql, name, binds)
+ )
+
+ result
+ end
end
「週次や月次バッチのような大量の処理をまとめてやる場合にGCで片付かないものがたまっていくというのは、いわゆるmemory bloat(メモリの肥大化)に相当すると思いますが、その結果メモリ不足で落ちるのはたしかに避ける方がいいので、少々キャッシュミスで再読み込みが発生する可能性があってもキャッシュに上限を設定しようという考えなんでしょうね」「なるほど」
参考: Active Support の Instrumentation 機能 - Railsガイド
🔗 HTMLのpicture
タグをサポート
HTMLのpictureタグをサポートするようになった。この機能ではString渡し、Array渡し、またはブロックがサポートされる。
imgタグに
:image
キーで直接プロパティを渡せるようになった。
pictureタグはimgタグを必要とするため、指定した最後の要素がimgタグに使われる。
ブロックを渡すことでpictureタグを完全に制御可能で、それに応じてタグの内容が入力される。以下のように単一ソースでも利用できる。
<%= picture_tag("picture.webp") %>
上は以下を生成する。
<picture> <img src="/images/picture.webp" /> </picture>
複数ソースの場合は以下のように書く。
<%= picture_tag("picture.webp", "picture.png", :class => "mt-2", :image => { alt: "Image", class: "responsive-img" }) %>
上は以下を生成する。
<picture class="mt-2"> <source srcset="/images/picture.webp" /> <source srcset="/images/picture.png" /> <img alt="Image" class="responsive-img" src="/images/picture.png" /> </picture>
ブロック渡しで完全に制御する場合:
<%= picture_tag(:class => "my-class") do %> <%= tag(:source, :srcset => image_path("picture.webp")) %> <%= tag(:source, :srcset => image_path("picture.png")) %> <%= image_tag("picture.png", :alt => "Image") %> <% end %>
上は以下を生成する。
<picture class="my-class"> <source srcset="/images/picture.webp" /> <source srcset="/images/picture.png" /> <img alt="Image" src="/images/picture.png" /> </picture>
Juan Pablo Balarini
同Changelogより
つっつきボイス:「HTMLの<picture>
タグ、そういえばありましたね」「ブラウザや端末によってはサポートしていない可能性がある画像や動画を別のものにフォールバックさせるタグのようですね↓」「ブラウザ側で選ぶためのヒントを提供するタグですか、なるほど」「こういう機能をRailsのタグヘルパーで使いたい人には嬉しいでしょうね」
参考: <picture>
: 画像要素 - HTML: HyperText Markup Language | MDN
<picture>
は HTML の要素で、0 個以上の<source>
要素と 1 つの<img>
要素を含み、様々な画面や端末の条件に応じた画像を提供します。
ブラウザーは複数の<source>
子要素を検討し、その中から最も適切なものを選択します。適切なものがない場合や、ブラウザーが<picture>
要素に対応してない場合、<img>
要素のsrc
属性で指定された URL が選択されます。選択された画像は<img>
要素が占有する領域に表示されます。
MDNより
<!-- MDNより-->
<!--Change the browser window width to see the image change.-->
<picture>
<source srcset="/media/cc0-images/surfer-240-200.jpg"
media="(orientation: portrait)">
<img src="/media/cc0-images/painted-hand-298-332.jpg" alt="">
</picture>
「そういえば以下のEvil Martiansの記事でもこの<picture>
タグが使われていました↓」
<!-- 同記事より -->
<picture>
<source
media="(max-width: 799px)"
srcset="cupcake.webp 1x, cupcake@2x.webp 2x"
type="image/webp"
>
<source
media="(min-width: 800px)"
srcset="huge-cupcake.webp 1x, huge-cupcake@2x.webp 2x"
type="image/webp"
>
...
<img src="cupcake.jpg" srcset="cupcake.jpg 1x, cupcake@2x.jpg 2x" alt="a yummy cupcake">
</picture>
🔗 assert_enqueued_email_with
にマッチャーを渡せるようになった
assert_enqueued_email_with
の引数やパラメータにprocを渡せるようになった。assert_enqueued_email_with DeliveryJob, params: -> p { p[:token] =~ /\w+/ } do UserMailer.with(token: user.generate_token).email_verification.deliver_later end
Max Chernyak
同Changelogより
つっつきボイス:「assert_enqueued_email_with
は以前も見かけたことがありますね(ウォッチ20220829): これをさらに改良して-> p { p[:token] =~ /\w+/ }
のようにマッチャーを渡せるようになったんですね、なるほど」
参考: Rails API assert_enqueued_email_with
-- ActionMailer::TestHelper
前編は以上です。
バックナンバー(2023年度第2四半期)
週刊Railsウォッチ: Rubyで環境変数を扱う、Web標準に「Baseline」ステータス追加ほか(20230531後編)
- 20230530前編 キャッシュシリアライザに
:message_pack
が追加、ViewComponent 3リリースほか - 20230525後編 Ruby 3.3.0-preview1リリース、in_order_ofのバグ修正ほか
- 20230524前編 withで作成したリレーションをjoinsで指定可能に、キャッシュストアの例外処理を統一ほか
- 20230502 スライド『Rails 7.1をn倍速くした話』、Rails 7.1でMessagePackをサポートほか
- 20230427後編 第1回Rails Worldが10月に開催、『研鑽Rubyプログラミング』でRuby本体も高速化ほか
- 20230425前編 Rails 7.1の複合主キー対応が引き続き進む、exceptメソッドにwithoutエイリアスが追加ほか
- 20230413後編 ShopifyのRubyパーサーyarp、RJITを書いた理由ほか
- 20230412前編 複合主キーの実装が進む、Rails公式のバグ再現用テンプレートほか
- 20230406後編 Rubyオブジェクトモデルクイズの最難問ほか
- 20230405前編 Arel::Nodes::NodeにAPIドキュメントが追加、rubocop-mdほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)