こんにちは、hachi8833です。
お知らせ: 週刊Railsウォッチ後編は都合により来週公開します。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 Ruby 3.3以上でRailsアプリを実行するとYJITがデフォルトで有効になる
Ruby 3.3以上で実行される新規アプリケーションでYJITがデフォルトで有効になる。
追加される
config/initializers/enable_yjit.rb
イニシャライザは、Ruby 3.3以上で実行される場合にYJITを有効にする。Jean Boussier
同CHANGELOGより
つっつきボイス:「この間7.1.2がリリースされたせいか今回のRails改修は比較的小粒な感じでしたが、その中でも目玉となる機能追加です」「お、config/initializers/enable_yjit.rbが新たに作られたんですね↓: RubyVM::YJIT.enable
な環境であればYJITが有効になる」「これで環境変数にRUBY_YJIT_ENABLE=1
を設定しなくてもデフォルトでYJITを有効にできますね」「DHHもBasecampやHEY.comでYJITを有効にしたことで高速化したとツイートしてる」
# railties/lib/rails/generators/rails/app/templates/config/initializers/enable_yjit.rb.tt
# Automatically enable YJIT as of Ruby 3.3, as it bring very
# sizeable performance improvements.
# If you are deploying to a memory constrained environment
# you may want to delete this file, but otherwise it's free
# performance.
if defined? RubyVM::YJIT.enable
Rails.application.config.after_initialize do
RubyVM::YJIT.enable
end
end
YJIT is going to be enabled by default in the next version of Rails when running on Ruby 3.3+. We saw an incredible 10-24% increase in performance on Basecamp and HEY after switching to YJIT. Ruby is so fast these days, it's great 🤘 https://t.co/d4viIDJM3Q
— DHH (@dhh) November 9, 2023
🔗 Active Record関連
🔗 ActiveRecord::Core#inspect
の出力をカスタマイズ可能になった
デフォルトでは、あるレコードで
inspect
を呼び出すと、id
のみを含むフォーマット済み文字列が生成される。Post.first.inspect #=> "#<Post id: 1>"
inspect
の出力に含めたい属性があれば、ActiveRecord::Core#attributes_for_inspect
で設定できる。Post.attributes_for_inspect = [:id, :title] Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"
attributes_for_inspect
を:all
に設定すると、inspect
でレコードの全属性が出力される。Post.attributes_for_inspect = :all Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
attributes_for_inspect
は、development環境とtest環境ではデフォルトで:all
に設定される。
full_inspect
を呼び出すことで全属性をinspect
することも可能。
attribute_for_inspect
で設定した属性はpretty_print
でも使われる。Andrew Novoselac
同CHANGELOGより
つっつきボイス:「今のRailsもconfig/initializers/filter_parameter_logging.rbでログに出したくないパターンをフィルタで除外できるけど、inspect
そのものをカスタマイズできるようにした、なるほど」「pretty_print
にも効くのか」「inspect
をカスタマイズ可能にした代わりに、従来のinspect
と同様に全属性を表示できるfull_inspect
も新たに追加されたんですね」「"ここだけは全部出したい"みたいなときに使うのかも」
「inspect
をカスタマイズするとしたら、inspect
で出したくないものがあるときとかでしょうか?」「production環境のRailsコンソールでinspect
したものにめちゃくちゃ長い文字列が入っていたりするとコンソールが壊れたりすることがあるので、そういうのを除外したくなるのはわかる」「そうそう、inspect
で壊れたときにデバッガーのページャーがオンになっていたりすると出力がいつまで経っても終わらないとかありますよね」「inspect
がカスタマイズ可能になったところに価値がある感じ👍」
🔗 変更されていないFloat::INFINITY
値をchangedとして保存しないよう修正
- PR: Don't mark
Float::INFINITY
as changed if didn't by MaicolBen · Pull Request #49904 · rails/rails
動機/背景
float
の無限値を含むレコードをsave
する場合はchangedとしてマークすべきではない。 レコードを変更せずに保存すると、そのたびに新しいバージョンが作成されるため、papertrailなどの監査システムで使うと面倒になる可能性がある。詳細
再現方法:
record = MyRecord.create!(float_field: Float::INFINITY) record.reload record.float_field = Float::INFINITY record.changed? # returns true when it should be false
変更の詳細:
- 問題は、数値を文字列に変換するときに
Inifinity
が数値でないため正規表現/\A\s*[+-]?\d/
にマッチしなかったこと。Inifinity
という文字列を正規表現に追加するのはfalse positiveになるのでよくないと思う。もっといいアイデアがあれば知らせて欲しい。同CHANGELOGより
つっつきボイス:「このプルリク見たことあるような気がする」「Inifinityという文字列が正規表現の数値判定にかからなかったので、is_a?(::Numeric)
で数値かどうかを先にチェックすることで修正したのね、なるほど」
# activemodel/lib/active_model/type/helpers/numeric.rb#L44
def number_to_non_number?(old_value, new_value_before_type_cast)
- old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
+ old_value != nil && !new_value_before_type_cast.is_a?(::Numeric) &&
non_numeric_string?(new_value_before_type_cast.to_s)
end
...
def non_numeric_string?(value)
# 'wibble'.to_i will give zero, we want to make sure
# that we aren't marking int zero to string zero as
# changed.
!NUMERIC_REGEX.match?(value)
end
NUMERIC_REGEX = /\A\s*[+-]?\d/
private_constant :NUMERIC_REGEX
「そういえばRubyのInfinityにはプラスのものとマイナスのものがあったはず」「たしか数学的にはInfinityとInfinityはイコールにならなかった気がする」「そもそもInfinityは特定の数値ではないんですよね」
参考: 無限 - Wikipedia
参考: Float::INFINITY
(Ruby 3.2 リファレンスマニュアル)
🔗 Arelのproc_for_binds
の振る舞いを修正
where(field: values)
クエリで、field
がシリアライズド属性の場合(field
でActiveRecord::Base.serialize
を使っている場合や、field
がJSONカラムの場合など)の振る舞いを修正した。João Alves
同CHANGELOGより
動機/背景
このプルリクは、issue #48535と#48072を修正する。
プルリク#41068の導入によって意図しない振る舞いが顕在化し、
proc_for_binds
が既にキャスト済みの属性にキャストを適用する原因となった。 その結果、上で参照しているissueで指摘されているようにクエリの誤動作が発生した。このコミットでは、型を
ActiveModel::Type.default_value
に置き換えて、2番目のシリアライズで事実上何も行わないようにすることで問題を修正する。この修正により、#41068以降壊れるようになった決定論的クエリを修正するなどの変更が不要になった(詳しくはこのコミット37361bfを参照(注: リンク切れ))。これらの変更により、本来HomogeneousIn
内のproc_for_binds
を対象とするはずのテストが、誤ってInWithAdditionalValues
内のproc_for_binds
を評価していたため、HomogeneousIn
内のproc_for_binds
を調整するたびに誤検知が発生していた。
同PRより
つっつきボイス:「お、Arelの修正だ」「カラムがシリアライズされている場合にエラーになっていたのを修正したらしい」「踏まないとなかなか気づけないバグ」
# activerecord/lib/arel/nodes/homogeneous_in.rb#L50
def proc_for_binds
- -> value { ActiveModel::Attribute.with_cast_value(attribute.name, value, attribute.type_caster) }
+ -> value { ActiveModel::Attribute.with_cast_value(attribute.name, value, ActiveModel::Type.default_value) }
end
「こういうJSONクエリを渡すと壊れたのね↓」
# activerecord/test/cases/serialized_attribute_test.rb#L244
def test_where_by_serialized_attribute_with_hash_in_array
settings = { "color" => "green" }
Topic.serialize(:content, type: Hash)
topic = Topic.create!(content: settings)
- assert_equal topic, Topic.where(content: [settings]).take
+ assert_equal topic, Topic.where(content: [settings, { "herring" => "red" }]).take
end
「HomogeneousIn
って何だろうと思ったらArelの内部クラスなんですね」「InWithAdditionalValues
もArelのモジュールだけど、このプルリクで削除されてますね」
参考: rails/activerecord/lib/arel/nodes/homogeneous_in.rb at main · rails/rails
🔗 Active Model関連
🔗 Active Recordのtype_for_attribute
をActive Modelに移動
Active Recordの
type_for_attribute
メソッドをActive Modelに移動した。ActiveModel::Attributes
をinclude
したクラスでこのメソッドが使えるようになる。振る舞いはActive Recordのときと同じ。class MyModel include ActiveModel::Attributes attribute :my_attribute, :integer end MyModel.type_for_attribute(:my_attribute) # => #<ActiveModel::Type::Integer ...>
Jonathan Hefner
同CHANGELOGより
つっつきボイス:「データベースに関係ない機能はActive RecordでもActive Modelでも同じように使えるようにしようという考えでtype_for_attribute
を移動したのかも」
🔗 BeforeTypeCast
モジュールをActive Modelに移動
ActiveModel::Attributes
をinclude
したクラスでは*_before_type_cast
や*_for_database
などの自動定義メソッドが使えるようになる。振る舞いはActive Recordのときと同じ。class MyModel include ActiveModel::Attributes attribute :my_attribute, :integer end m = MyModel.new m.my_attribute = "123" m.my_attribute # => 123 m.my_attribute_before_type_cast # => "123" m.read_attribute_before_type_cast(:my_attribute) # => "123"
Jonathan Hefner
同CHANGELOGより
つっつきボイス:「これもActive Modelへの引っ越しです」「*_before_type_cast
や*_for_database
をActive Modelだけでも使えるようにするのはわかる: 特に型キャスト周りの機能はActive Modelで欲しいときもあるので」「Active Recordだとできるのに、Active Modelだとできそうでできなかったりするとちょっと悲しいですよね」
「Active Modelの属性周りがだんだんリッチになってきた感じですね」「どちらかというとActive Modelのインターフェイスや振る舞いをActive Recordに近づけている感じかな: RailsでPORO(Pure Old Ruby Object)的なものが欲しいときは基本的にActive Modelを使えばいいと思いますが、Rails以外のRubyアプリで似たようなことをしたいならdry-rbのdry-structとかを使う選択肢もありますね(ウォッチ20200226)」
参考: dry-rb - Home
「Active Modelはsave
で永続化しないときに使うのかと思っていました」「Active Modelでもデータを永続化したければActive Recordに合わせてsave
系メソッドを自分で実装すればActive Record透過な使い方ができますし、Active Modelの#persisted?
なども元々そういう使い方のために用意されているメソッドですね(Active Modelの#persisted?
はデフォルトでは常にfalse
を返す)」「あ、そうなんですね」「Active Modelは本来データベースと密結合する機能はなくて良いはずですが、今回のようにActive Recordとインターフェースを合わせるために*_for_database
系のtype casting機能を統合する過程で、Active Modelでもデータベースを前提とした機能になりつつあるというのは興味深いですね」
「Active Modelのインターフェイスは以前からActive Recordに近いものになっていますけど、こういう改修を見ていると両者の機能やインターフェイスをもっと近づけて相互運用可能にする方向に進んでいるのかなという気がしますね」「なるほど、そうなると今後もこの種の仕様変更が増えるかもしれませんね」「Active ModelとActive Recordのインターフェイス統一が進むと、たとえば最初はActive Modelで書いたシンプルなモデルを後からActive Record化するみたいな作業が多少やりやすくなるんじゃないかな(それほど簡単ではないと思いますが)」
参考: Active Model の基礎 - Railsガイド
🔗 Action Mailer関連
🔗 Action Mailerのプレビューでインライン添付ファイルを別表示にした
Action Mailerのプレビューで、インライン添付ファイルを通常の添付ファイルと分けて表示するようになった。
例:Attachments: logo.png file1.pdf file2.pdf
上は以下のように表示される。
Attachments: file1.pdf file2.pdf (Inline: logo.png)
Christian Schmidt and Jonathan Hefner
同CHANGELOGより
つっつきボイス:「インライン添付ファイルをデフォルトで他の添付ファイルと分けて表示する: 小さいけど便利な修正ですね👍」
参考: §2.3.2 ファイルをインラインで添付する -- Action Mailer の基礎 - Railsガイド
🔗 メールにDate
ヘッダーが存在する場合はAction Mailerのプレビューで日付を表示
Action Mailerのプレビューで、メッセージの
Date
ヘッダーが存在する場合は日付を表示するようになった。Sampat Badhe
同CHANGELOGより
つっつきボイス:「これもAction Mailerの改修ですね」「修正前のTime.current
を表示する方法よりはDate
ヘッダーを使う方がいいでしょうね👍」
# railties/lib/rails/templates/rails/mailers/email.html.erb#L95
<dt>Date:</dt>
- <dd id="date"><%= Time.current.rfc2822 %></dd>
+ <dd id="date"><%= @email.header['date'] || Time.current.rfc2822 %>
参考: § 2.6 メールのプレビュー -- Action Mailer の基礎 - Railsガイド
🔗 SMTP-To:
を必要な場合にのみ表示するよう修正
Action Mailerのプレビューで、
To
とCc
とBcc
の和集合と一致しない場合にのみSMTP-To:
を表示するようになった。Christian Schmidt
同CHANGELOGより
つっつきボイス:「メールの送信先リストがSMTP-To:
と一致しない場合にのみSMTP-To:
を表示、なるほど」「個人的にはメールのヘッダーを加工せずに全部表示してくれる方がトラブルシューティングがやりやすいかなと思いますけどね」
同PRより
🔗 Action TextのJavaScriptがSprocketsでうまく動かない問題を修正
- actiontext.esm.jsとしてブラウザで直接利用される可能性があるESMパッケージをコンパイルするよう修正。
Matias Grunberg
- Sprocketsでactiontext.jsが正常に動作しない問題を修正。
Matias Grunberg
同CHANGELOGより
つっつきボイス:「先週も似たような修正がありましたね」「TrixのJavaScriptのESMがSprocketsでうまく動かない問題が修正されたやつですね(ウォッチ20231114)」「他にもJavaScript周りで修正があるかも?」
🔗 システムテストをパラレル実行したときの競合状態を修正
パラレル実行されるシステムテストで
Text file busy - chromedriver
エラーが発生する可能性のある競合状態を修正。Matt Brictson
同CHANGELOGより
つっつきボイス:「chromedriverのパスのプリロードがパラレルテストで失敗することがあったのね」「マルチプロセスでパスをプリロードするのは、ワーカーがforkする前にOSの共有メモリにパスを読み込んでおくことで効率を高めるためなんでしょうね」「修正はシンプルだけどバグの原因究明は大変そう」
# actionpack/lib/action_dispatch/system_testing/browser.rb#L31
def preload
case type
when :chrome
- ::Selenium::WebDriver::Chrome::Service.driver_path.try(:call)
+ resolve_driver_path(::Selenium::WebDriver::Chrome)
when :firefox
- ::Selenium::WebDriver::Firefox::Service.driver_path.try(:call)
+ resolve_driver_path(::Selenium::WebDriver::Firefox)
end
end
...
+ def resolve_driver_path(namespace)
+ namespace::Service.driver_path = ::Selenium::WebDriver::DriverFinder.path(
+ options || namespace::Options.new,
+ namespace::Service
+ )
+ end
動機/背景
webdrivers
が存在しない場合(これはRails 7.1以降ではデフォルトのシナリオ)、Seleniumのdriver_path
は最初nil
になる。つまり、ドライバのパスはlazyに検索され、システムテストが実行されるまで先送りされることになる。これによって、パラレルテストで各ワーカープロセスが同時にドライバを解決しようとして競合状態が発生する。その結果、#49906のエラーが発生する。
詳細
このコミットでは、
Browser#preload
の実装を変更することで競合状態を修正する。変更前の実装ではdriver_path
がwebdrivers
gemによってProcに設定されていたが、webdrivers
gemが使われなくなってdriver_path
がnil
になるとうまくいかなくなる。修正後の
Browser#preload
では、selenium-webdriver
が提供するDriverFinder
を用いてdriver_path
がnil
の場合にドライバのパスをeagerに解決するようになった。これにより、パラレルテストのワーカーがforkする前にdriver_path
が設定されるようになる。追加情報
このソリューションは、Selenium内でドライバの実行ファイルを必要に応じて検出するコードを真似ている。
service.executable_path ||= WebDriver::DriverFinder.path(options, service.class)
同PRより
前編は以上です。
バックナンバー(2023年度第4四半期)
週刊Railsウォッチ: RBS 3.3.0とSteep 1.6.0リリース、Ruby Conf Taiwan 2023ほか(20231116後編)
- 20231114前編 MariaDBのRETURNINGオプションをサポートほか
- 20231107 Kaigi on Rails発表「Simplicity on Rails」を見るほか
- 20231025後編 ShopifyのWebAssemblyツールチェインRuvyほか
- 20231024前編 7.1アップグレードガイドにActive Record暗号化設定の注意事項が追加ほか
- 20231018後編 Kaigi on Rails 2023関連イベント情報公開、複合主キーのlocality解説記事ほか
- 20231017前編 Active Storageのしくみを詳しく解説するDiscussion投稿ほか
- 20231011 Rails 7.1.0リリース、YARPがprismにリネームほか
- 20131004 Rails 7.1.0.rc1と7.1.0.rc2がリリース、SQLite3コンフィグの最適化ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)