- Ruby / Rails関連
週刊Railsウォッチ: RailsのRuby 3.2.0対応、ActiveSupport::Durationの暗黙の変換ほか(20221220前編)
こんにちは、hachi8833です。今年最後の週刊Railsウォッチ前編をお送りいたします。
🔗Rails: 先週の改修(Rails公式ニュースより)
つっつきボイス:「ついでにマイルストーンを見てみると、現時点ではあと5個でした↓」「でもまだv7.1.0
タグができてないし、それらしい動きもまだ見られないので、Rails 7.1はまだ先でしょうね」「7.0登場後まだ1年ですしね」
🔗 has_rich_text
にstrict_loading:
オプションが追加
has_rich_text
のシグネチャを拡張してstrict_loading:
値を受け取れるようにする。この値は、背後のhas_one
宣言に転送される。このオプションを省略すると、strict_loading:
にはstrict_loading_by_default
クラス属性の値が設定される(デフォルトはfalse
)。
同PRより
つっつきボイス:「リッチテキストなのでAction Textの改修ですね」「事情はわからないけどstrict loadingしたいことがあったのかも」
# actiontext/lib/action_text/attribute.rb#L33
- def has_rich_text(name, encrypted: false)
+ #
+ # * <tt>:strict_loading</tt> - Pass true to force strict loading. When
+ # omitted, <tt>strict_loading:</tt> will be set to the value of the
+ # <tt>strict_loading_by_default</tt> class attribute (false by default).
+ def has_rich_text(name, encrypted: false, strict_loading: strict_loading_by_default)
...
rich_text_class_name = encrypted ? "ActionText::EncryptedRichText" : "ActionText::RichText"
has_one :"rich_text_#{name}", -> { where(name: name) },
- class_name: rich_text_class_name, as: :record, inverse_of: :record, autosave: true, dependent: :destroy
+ class_name: rich_text_class_name, as: :record, inverse_of: :record, autosave: true, dependent: :destroy,
strict_loading: strict_loading
参考: Action Text の概要 - Railsガイド
🔗 clear_query_caches_for_current_thread
がすべてのコネクションで単一のロックを使うようにする
フォローアップ: #46519
フォローアップ: #46553
修正: #45994マルチプルデータベースを使う場合、
clear_query_caches_for_current_thread
はすべてのコネクションプールを通過し、1つずつロックを取得して各クエリキャッシュをクリアしなければならない。
2つのスレッドが異なる順序でこれを行うと、デッドロックする可能性がある。
この問題は、すべてのコネクションで単一のロックを使うことで完全に回避されるようになり、pumaのスレッドとメインスレッドの競合を正しく防げるようになる。
以前のプルリクのように、このロックをRackミドルウェアに持たせて、visit
などのCapybaraプリミティブが呼び出されたらロックを解放する方がすっきりするのだが、そのためにはCapybaraに手頃なチョークポイントが必要で、アップストリームでそれを探る必要がある。
@eileencodes @matthewd @kuahyeow
同PRより
参考: Rails API clear_query_caches_for_current_thread
-- ActiveRecord::ConnectionHandling
つっつきボイス:「先週見た$46553のスレッド周りのロックの修正と似ているかも(ウォッチ20221213)」「たしかにこちらもThredの場合とFiberの場合で処理を分けていますね」「まさしく先週の続き」「ActiveSupport::Concurrency
のThreadLoadInterlockAwareMonitor
とLoadInterlockAwareMonitor
を使い分けている」
# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L175
+ THREAD_LOCK = ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
+ private_constant :THREAD_LOCK
+
+ FIBER_LOCK = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+ private_constant :FIBER_LOCK
+
def lock_thread=(lock_thread) # :nodoc:
@lock =
case lock_thread
when Thread
-- ActiveSupport::Concurrency::ThreadLoadInterlockAwareMonitor.new
+ THREAD_LOCK
when Fiber
- ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+ FIBER_LOCK
else
ActiveSupport::Concurrency::NullLock
end
end
参考: ActiveSupport::Concurrency::LoadInterlockAwareMonitor
🔗 オブジェクトのnew
を避ける最適化3件
- commit: Avoid creating new Array object per each comparison · rails/rails@d8c0504
- commit: Defer creation of Request object until the object is in need · rails/rails@c0f16c1
- Prefer Kernel#sprintf over String#% · rails/rails@9c66072
つっつきボイス:「amatsudaさんによる最適化が似たような内容だったのでまとめてみました」
「[:js]
という配列リテラルだと配列オブジェクトが作成されるので、それを避ける書き方にしたんですね↓」
# actionview/lib/action_view/lookup_context.rb#L259
def formats=(values)
if values
values = values.dup
values.concat(default_formats) if values.delete "*/*"
values.uniq!
invalid_values = (values - Template::Types.symbols)
unless invalid_values.empty?
raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
end
- if values == [:js]
+ if (values.length == 1) && (values[0] == :js)
values << :html
@html_fallback_for_js = true
end
end
super(values)
end
「こちらもActionDispatch::Request.new
を後回しにして、必要がなければオブジェクトを生成しないようにしている↓」「途中でreturn
する可能性があるならその後でnew
する方がいいですね」
# actionpack/lib/action_dispatch/http/content_security_policy.rb#L34
def call(env)
- request = ActionDispatch::Request.new env
status, headers, _ = response = @app.call(env)
# Returning CSP headers with a 304 Not Modified is harmful, since nonces in the new
# CSP headers might not match nonces in the cached HTML.
return response if status == 304
return response if policy_present?(headers)
+ request = ActionDispatch::Request.new env
+
if policy = request.content_security_policy
nonce = request.content_security_policy_nonce
nonce_directives = request.content_security_policy_nonce_directives
context = request.controller_instance || request
headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
end
response
end
「引数の%
メソッドに配列リテラルを渡すと評価順序では先に配列オブジェクトが生成されるので、配列リテラルを使わなくて済むsprintf
メソッドに変更している↓」「そういえば%
はStringクラスのメソッドでしたね」「こういう地道なリファクタリングを積み重ねるのが後々効いてくる👍」
# railties/lib/rails/rack/logger.rb#L48
def started_request_message(request) # :doc:
- 'Started %s "%s" for %s at %s' % [
+ sprintf('Started %s "%s" for %s at %s',
request.raw_request_method,
request.filtered_path,
request.remote_ip,
- Time.now.to_default_s ]
+ Time.now.to_default_s)
end
🔗 Railsガイド: STIのプリロード方法を4とおり記述
つっつきボイス:「こちらはドキュメントへの追記です」「こういう改修も地道だけど重要ですね👍」
参考: §5 シングルテーブル継承 (STI) -- Active Record の関連付け - Railsガイド
🔗 SQLite3のdbファイルはdb/ではなくstorage/に置くことにする
db/ディレクトリはデータ用ではなくコンフィグ専用であるべき。そうすることで、test環境やdevelopment環境はもちろん、production環境でも単一のデータボリュームをコンテナにマウントしやすくなる。
同PRより
つっつきボイス:「運用中に更新されるデータファイルをdb/ディレクトリに置くのが違和感があるというのはわかる」「db/はコンフィグやseedみたいに通常は更新されないものを置くということですね」「データの置き場所をstorage/に定める方がコンテナでボリュームマウントがしやすくなりますね」
「storage/ディレクトリはこれまで標準にはなかったと思うけど追加されたんですね」「こういう変更を突然入れてくるところがいかにもDHHらしい」「通常のアプリには影響なさそうだけどRailsチュートリアルのような教材には影響ありそう」
🔗 #last
と#first
のORDER BYでquery_constraints
を使うようにする
#46331で最近導入された
query_constraints
コンフィグは、既存のActiveRecord::Base#primary_key
をさらに抽象化して「仮想の主キー」として扱えるので、今後Active Recordモデルのレベルではprimary_key
をquery_constraints_list
に置き換える方向に進むはず。このプルリクは、
#last
と#first
というfinderメソッド(どちらも内部でordered_relation
メソッドを呼び出す)を変更して、ORDER BY句のビルドにquery_constraints_list
を使うようにする。既存のすべてのActive Recordモデルは、query_constraints_list
を[primary_key]
として暗黙で設定する(#46439)ので、そうしたモデルのほとんどは[id]
と同じになるので、この変更はbreaking changeではない。今後行われそうな拡張
このリファクタリング中に、implicit_order_column
をimplicit_order_columns
に変更してORDER BY句で複数のカラムを定義できるようにすれば比較的簡単にやれることに気づいた。自分は今すぐこれをやる必要は感じておらず、単に簡単にやれそうだということを述べたいだけである。
同PRより
つっつきボイス:「query_constraints
は少し前に追加されたコンフィグですね(ウォッチ20221115)」「複合主キーを使う場合はORDER BYをそれぞれのキーにも効かせないと並び順が不定になってしまう: ここではfirst
とlast
のときにもそれを行うようにしたということですね」「こんな場所にも影響してくるとは」「これがないと、first
やlast
のたびに値が変わってしまう可能性があるので、必要な修正ですね👍」
# activerecord/lib/active_record/relation/finder_methods.rb#L578
def ordered_relation
- if order_values.empty? && (implicit_order_column || primary_key)
- if implicit_order_column && primary_key && implicit_order_column != primary_key
- order(table[implicit_order_column].asc, table[primary_key].asc)
- else
- order(table[implicit_order_column || primary_key].asc)
- end
+ if order_values.empty? && (implicit_order_column || !query_constraints_list.empty?)
+ # use query_constraints_list as the order clause if there is no implicit_order_column
+ # otherwise remove the implicit order column from the query constraints list if it's there
+ # and prepend it to the beginning of the list
+ order_columns = implicit_order_column.nil? ? query_constraints_list : ([implicit_order_column] | query_constraints_list)
+ order(*order_columns.map { |column| table[column].asc })
else
self
end
end
🔗 ActiveRecord::Calculations#ids
が返すidが重複する問題を修正
ActiveRecord::Calculations#ids
が一意のidのみを返すようにする
eager_load
やpreload
やincludes
を使う場合にベースモデルが一意のidリストのみを返すようにActiveRecord::Calculations#ids
を更新した。# (修正後の動作) Post.find_by(id: 1).comments.count # => 5 Post.includes(:comments).where(id: 1).pluck(:id) # => [1, 1, 1, 1, 1] Post.includes(:comments).where(id: 1).ids # => [1]
つっつきボイス:「ActiveRecord::Calculations#ids
が重複する値を返すことがあるのは仕様なのかと思ってた」「最近これを踏んだコードをレビューで指摘した覚えがある」「find_by
/ preload
/ includes
したときにidカラムが重複するのは前からありましたね」「修正後は、pluck(:id)
すると重複するけど、ids
なら最初から重複しない値を取れるようになったんですね、なるほど👍」
🔗 番外: ActiveSupport::Duration
の暗黙の変換を非推奨化
- PR: Deprecate implicit convertion of Duration by skipkayhil · Pull Request #45437 · rails/rails -- 現在オープン
概要
Durationにmethod_missing
が存在することで、ときどき非常に混乱する振る舞いが生じる。1.year.days # => 31556952 Days
7bd9603やa2535d9でDurationを特定の単位に変換するメソッドが追加されたが、そのときに置き換えるはずだった混乱するメソッドが残っていた。
この変更では
method_missing
を非推奨とし、残す意味のあるメソッドを再実装または明示的に委譲することで修正する。
さらに、Durationには*
のような直感に反した振る舞いをするメソッドもいくつかある。そうしたメソッドにおける特定のケース(Duration * Duration))についても非推奨とした。1.day * 2.days # => 172800 days
修正: #45433
同PRより
つっつきボイス:「これはまだオープンなんですが、1.year.days # => 31556952 Days
という挙動があることにびっくりしたので拾ってみました」「何と」「手元で動かしてみたら本当にこうなりました」「day
やyear
が値を秒で返しているっぽい」「その値のdays
を取るとこんなでかい値になっちゃうのね」
「コメントにこんな書き方がありますね↓」「Durationの剰余を取れるってありなのか🤔」「取りたい気持ちはわかる」
# 同コメントより
5.minutes % 120 # => 1.minute
7.seconds % 3.seconds # => 1.second
7.minutes % 3 # => 0
# because 420 seconds is divisible by 3
🔗Rails
🔗 RailsのRuby 3.2対応
つっつきボイス:「amatsudaさんやyahondaさんがRailsのRuby 3.2対応作業をやっていたので拾いました」「たまたまamatsudaさんがRailsに変更をプッシュしたら3.2の問題をいくつか踏んでしまったのか」「rakeのバージョンが戻るのはつらそう...」
その後他にもいくつか修正が入りました。
- PR: YJIT: Fix
obj.send(:call)
by XrXr · Pull Request #6943 · ruby/ruby - PR: Fix unintentional downgrades when gemspec DSL is used by deivid-rodriguez · Pull Request #6131 · rubygems/rubygems
- PR: zeitwerk_integration_test.rb fails against yjit-enabled Ruby 3.2.0dev · Issue #46734 · rails/rails
「ところで、これに関連するやりとりに出てきた#46712を見ると↓、RubyVM::class_serial
がRuby 3.2からなくなったらしい」「手元でやってみると、たしかになくなっていますね」「こちらも3.2.0-rc1をビルドしてやってみるとRubyVM::stat
の内容も変わっている」「Rubyで普段使わないところがこんなふうに変わっているんですね」
- issue:
ExecutorTest#test_class_serial_is_unaffected
testing with Ruby 3.2 · Issue #46712 · rails/rails
3.2.0rc1$ irb
>> defined?(RubyVM::class_serial)
=> nil
このコードでは、
class_serial
が前回のアクセス時に記録された「期待されるclass_serial
」と同じであることを確認していることがわかります。class_serial
はクラスのバージョン番号で、クラスが変更されるとインクリメントされます。また、オブジェクトのサイズが同じクラスの他のインスタンスのサイズと異なる可能性もあるので、必要な数のインスタンス変数スロットがオブジェクトにあるかどうかもチェックする必要があります。
Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)より
🔗 Pay: 複数のpayment processorを扱えるgem(Ruby Weeklyより)
つっつきボイス:「以下のpayment processorを扱えるgemだそうです」
- Stripe (SCA Compatible using API version 2022-11-15)
- Paddle (SCA Compatible & supports PayPal)
- Braintree (supports PayPal)
- Fake Processor (used for generic trials without cards, free subscriptions, testing, etc)
「ところで、このリストに日本の決済代行業者がリストアップされそうにないのが残念」「日本のサービスだと難しい点があるんでしょうか?」「最近はそうでもないかもしれませんが、日本に以前からある決済代行業者の多くはAPI仕様がクローズドになっていて、契約しないと読めない場合や、無料登録でAPI仕様は読めてもAPI仕様を他言できない場合もあったりするんですよ」「だから自前でAPIアクセスを実装しないといけなくなるのか...😢」「昔は日本にもオープン仕様の決済代行サービスがあった覚えがあるんですが、吸収合併か何かでなくなったんじゃなかったかな」「残念ですね」「日本のエンタープライズ方面では、仕様を公開するとセキュアでなくなるのではないかという考え方が今も根強いんですよ」
🔗 Railsの機能で認証と認可を実装する(Ruby Weeklyより)
つっつきボイス:「Deviseなどを使わずにRailsの機能で認証と認可を実装する記事と、そのgemだそうです」「gemになっている時点で既にRailsだけの機能ではなくなっていると思いますけどね: 記事を見た感じではDeviseで実装するのと複雑さはあまり変わらない感じかな」「なるほど」「たしかにDeviseで書くのは美しいとは言い難いコードになってしまうので、こういうものを作りたくなる気持ちもわかる」
前編は以上です。
バックナンバー(2022年度第4四半期)
週刊Railsウォッチ: Ruby 3.2.0 RC1がリリース、YARVアドベント記事、ChatGPTほか(20221214後編)
- 20221213前編 『RubyとRailsの何が強いのか』、書籍『Ruby on Railsステップアップ』ほか
- 20221207後編 JRubyが9.4.0.0でRuby 3.1に対応、IRB v1.5.0リリースほか
- 20221206前編 月刊のHotwireニュースレター、pessimize gemほか
- 20221130後編 Ruby 3.2のParser目玉機能ほか
- 20221129前編 Hanami 2.0リリース、Railsに関わる技術の体系化を目指した本ほか
- 20221122 The Rails Foundation発足、Ruby 3.2.0 Preview 3リリース、Ruby演算子クイズほか
- 20221116後編 Rubyを使っている企業の時価総額リスト、irbのshow_source、GitHub Codespacesほか
- 20221115前編 RailsチュートリアルがRails 7対応版をリリース、ViewComponentで使えるLookbookほか
- 20221102後編 書籍『Programming Ruby 3.2 (5th Edition)』、ReDoSチェックサイトほか
- 20221101前編 Packwerkの詳しい解説書『Gradual Modularization for Ruby and Rails』ほか
- 20221026後編 Ruby 3.2のData.define、RubyPrize 2022最終ノミネート、Puma-dev gemほか
- 20221025前編 rodauth-rails gem作者の解説記事、turbo-railsの有料チュートリアルほか
- 20221019後編 Ruby技術者認定試験再受験無料キャンペーン、Starlink日本で販売開始ほか
- 20221018前編 Rails向けLanguage Server “refreshing”開発中、JetBrains Fleetほか
- 20221012後編 RailsとPostgreSQLで列挙型を作成する6つの方法、Ubuntu Proほか
- 20221011前編 Turbo 7.2.0リリース、GitLabのDevSecOpsサーベイ結果ほか
- 20221004後編 ヒアドキュメント拡張の提案、『組織に自動テストを根付かせる戦略』ほか
- 20221003前編 Kaigi on Rails 2022のタイムテーブル発表、書籍『Practicing Rails』ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)