- Ruby / Rails関連
週刊Railsウォッチ: AR::RelationにCTEを利用できるwithメソッドが追加、Propshaftアップグレードガイドほか(20220711前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
Changelogをチェックした後で、公式更新情報も出ていたことに気づきました↓。既に扱ったものも結構あるようなので、次回見ようと思います。
参考: Ruby on Rails — Improved PostgreSQL support, performance improvements and more...
🔗 Active Recordのin_batches
でuse_ranges: true
を指定可能になった
- Active Recordにおけるテーブル全体イテレーションのバッチを最適化
従来の
in_batches
は、すべてのidを受け取ってからバッチごとにINベースのクエリを構築していた。これはテーブル全体をイテレーションする場合に最適ではない(不要なidが読み込まれるうえに、項目数の多いINクエリは遅くなる)。
この改修によって、テーブル全体のイテレーションがデフォルトでrangeによるイテレーション(id >= x AND id <= y
)を使うようになり、イテレーションが数倍高速になる。たとえば、PostgreSQLで1000万件のレコードでテストした場合、読み出しが253s
->30s
、更新が288s
->124s
、削除が268s
->83s
となった。この方法がデフォルトで使われるのは、テーブル全体のイテレーションのみ。この振る舞いは、
use_ranges: false
を渡すと無効にできる。
たとえば、テーブル全体をイテレーションするときの条件がarchived_at: nil
のみで、かつアーカイブ済みレコードの件数がわずかしかないような場合は、以下のようにこの方法を採用するのが理にかなっている。
Project.where(archived_at: nil).in_batches(use_ranges: true) do |relation|
# 何かする
end
詳しくは#45414を参照。
fatkodima
同Changelogより
つっつきボイス:「in_batch
で1つのテーブル全体を操作する場合にid >= x AND id <= y
のようにrangeで取得すれば速くなる、たしかに」「use_ranges: true
を指定するとrangeで取れるようになるんですね」
「ところでこの方法は、idカラムがintegerでオートインクリメントされていることが前提だと思うので、idがたとえばUUIDだとうまくいかないでしょうね」「それ今同じこと思いました」「エンタープライズ寄りの案件だとidがUUIDになっていることが最近増えているので、そういうときは注意が必要かな」
参考: UUID - Wikipedia
🔗 ActiveRecord::Relation
にCTEを利用できるwith
メソッドが追加
- PR: Common Table Expression support added "out-of-the-box" by vlado · Pull Request #37944 · rails/rails
このプルリクで追加される
ActiveRecord::Relation::QueryMethods#with
を使うと、CTE(Common Table Expressions)による複雑なクエリを非常に手軽にビルドできるようになる。
基本的には、渡された引数をArel::SelectManager.with
で期待されている方法で「ラップ」しているだけ。これによる最も大きなメリットは以下のとおり。
Arel::Nodes::As
ノードを手動でビルドせずに、標準的なActive RecordリレーションだけでやれるようになるActiveRecord::Relation
が返されるので柔軟性が失われない以下の例を参照のこと。
Post.with(
posts_with_comments: Post.where("comments_count > ?", 0),
posts_with_tags: Post.where("tags_count > ?", 0)
)
# 上は以下の従来コードを置き換えられる
posts_with_comments_table = Arel::Table.new(:posts_with_comments)
posts_with_comments_expression = Post.arel_table.where(posts_with_comments_table[:comments_count].gt(0))
posts_with_tags_table = Arel::Table.new(:posts_with_tags)
posts_with_tags_expression = Post.arel_table.where(posts_with_tags_table[:tags_count].gt(0))
Post.all.arel.with([
Arel::Nodes::As.new(posts_with_comments_table, posts_with_comments_expression),
Arel::Nodes::As.new(posts_with_tags_table, posts_with_tags_expression)
])
# WITH posts_with_comments AS (
# SELECT * FROM posts WHERE (comments_count > 0)
# ), posts_with_tags AS (
# SELECT * FROM posts WHERE (tags_count > 0)
# )
# SELECT * FROM posts
同PRより
つっつきボイス:「ActiveRecord::Relation
でCTEのWITH句が使えるようになる🎉」「おほ、いい書き方❤️」
「with
を使うと冒頭のサンプルのように4行で書けるのね」「その下にある同等のコードを見ると、処理ごとにArel::Table.new
を生成していますね」「Arelテーブルとして生成する、なるほど」「WITH句を使うときに与える必要のある名前も指定できる」「しかもPost.where("comments_count > ?", 0)
のような部分は普通のActive Recordで書けるので、生SQLで書く場合と違ってちゃんと型解決されるのはうまくできてますね」「そうそう、生SQLだと型解決忘れがち😅」
「CTEが公式に使えるだけでもありがたいし、ActiveRecord::Relation::QueryMethods#with
でこういう書き方ができるなら使いやすそう👍」「Arel自体には前からwith
が定義されているので、従来コードは今でも書けるはずですね」「with
で書ける方が見やすい」「こっち使いたい」
「よく見るとこのプルリクは2019年にオープンされてますね」「3年越しか〜」
🔗 ActiveSupport::Deprecation
の振る舞いをカスタマイズしたときのNoMethodErrorを修正
ActiveSupport::Deprecation.behavior=
は、call
に応答する任意のオブジェクトを受け取るとされていたが、実際の内部実装はオブジェクトがarity
にも応答することを前提としていたため、Proc
オブジェクトに限定されていた。
この変更によって、振る舞いをカスタマイズしたときのarity
制約がなくなった。
Ryo Nakamura
同PRより
つっつきボイス:「ActiveSupport::Deprecation
でNoMethodErrorが起きていたのが修正された」「エラーで止まってしまうのは勘弁して欲しいヤツですね」
🔗 rails new
の暗黙のオプションの扱いを改善
このコミットは、
AppGenerator
とPluginGenerator
の暗黙のオプションの扱いをさまざまな点で改良する。
暗黙のオプションは以下のように出力されるようになる。
$ rails new my_cool_app --skip-active-job
Based on the specified options, the following options will also be activated:
--skip-action-mailer [due to --skip-active-job]
--skip-active-storage [due to --skip-active-job]
--skip-action-mailbox [due to --skip-active-storage]
--skip-action-text [due to --skip-active-storage]
...
オプション同士がコンフリクトしている場合はエラーを出力する。
$ rails new my_cool_app --skip-active-job --no-skip-active-storage
Based on the specified options, the following options will also be activated:
--skip-action-mailer [due to --skip-active-job]
--skip-active-storage [due to --skip-active-job]
ERROR: Conflicts with --no-skip-active-storage
--skip-action-mailbox [due to --skip-active-storage]
--skip-action-text [due to --skip-active-storage]
railties/lib/rails/generators/app_base.rb:206:in `report_implied_options': Cannot proceed due to conflicting options (RuntimeError)
--minimal
などのメタオプションによる暗黙のオプションも具体的に出力される。
$ rails new my_cool_app --minimal
Based on the specified options, the following options will also be activated:
--skip-active-job [due to --minimal]
--skip-action-mailer [due to --skip-active-job, --minimal]
--skip-active-storage [due to --skip-active-job, --minimal]
--skip-action-mailbox [due to --skip-active-storage, --minimal]
--skip-action-text [due to --skip-active-storage, --minimal]
--skip-javascript [due to --minimal]
--skip-hotwire [due to --skip-javascript, --minimal]
--skip-action-cable [due to --minimal]
--skip-bootsnap [due to --minimal]
--skip-dev-gems [due to --minimal]
--skip-system-test [due to --minimal]
...
--no-*
オプションをメタオプションと組み合わせることが可能になり、すべてのオプションのリストが正確に出力される。
$ rails new my_cool_app --minimal --no-skip-active-storage
Based on the specified options, the following options will also be activated:
--skip-action-mailer [due to --minimal]
--skip-action-mailbox [due to --minimal]
--skip-action-text [due to --minimal]
--skip-javascript [due to --minimal]
--skip-hotwire [due to --skip-javascript, --minimal]
--skip-action-cable [due to --minimal]
--skip-bootsnap [due to --minimal]
--skip-dev-gems [due to --minimal]
--skip-system-test [due to --minimal]
...
つっつきボイス:「先週の--minimal
オプションの改修(ウォッチ20220704)の続きっぽい」「スキップするオプション同士のコンフリクトが検出されるようになったり、--minimal
のようなメタオプションで何がスキップされるのかを具体的に表示されるようになった」「言われてみれば何がスキップされるのか割と謎でしたね」
「rails new
のオプションなので通常の開発で使う頻度は低いと思いますが、オプション同士がコンフリクトしたときにソースコードを追わなくても原因がわかるのはいい👍」「自分はrails new
をしょっちゅう使うのでありがたいです🙏」
🔗 cookieを設定したときにSameSite
属性をオプトアウト可能になった
#37974以降、
SameSite
属性をcookieから除外できなくなった。SameSite
はたしかに推奨フィールドだが必須ではないので、オプトアウトも可能にすべき。
このプルリクは、以下のようにsame_site: nil
を指定することでオプトアウトを可能にする。
cookies[:foo] = { value: "bar", same_site: nil }
従来は上を指定すると、
cookies_same_site_protection
設定の値でSameSite
属性が誤った形で設定されていた。#44934で追加されたドキュメントにはnil
を値として渡せると書かれているが、そうするとデフォルト値(:lax
)にフォールバックしてしまう。
同PRより
つっつきボイス:「cookieのSameSite
属性をオプトアウトできるようになったそうです」「フレームワークとしてはデフォルトでSameSite
属性を有効にしたうえでオプトアウト可能にするのが望ましいでしょうね」「オプトアウトしたいのはどんなときだろう🤔」
参考: SameSite cookies - HTTP | MDN
🔗Rails
🔗 fixtureについて話しておきたい
つっつきボイス:「自分がfixtureが好きな理由のひとつとして、fixtureを即物的に書いていればコードによって変化しないというのがあります: でもテストが複雑になったときに以下の<%= User::B2C_TYPE %>
みたいにfixtureの中にfactory_botみたいなものを書くぐらいだったら、factory_botでロジカルに生成する方がいいというのもわかる」
# 同記事より
# spec/fixtures/users.yml
matheus:
first_name: Matheus
last_name: Sales
role: admin
type: <%= User::B2C_TYPE %>
# spec/fixtures/roles.yml
admin:
name: admin
user:
name: user
「自分は割とfixtureが好きなんですけど、周りを見るとfactory_botで書くのが好きな人も多いですね」「factory_bot使いますっ」「使いますっ」「ロジカルに書くならfactory_botだと思うけど、ベタに列挙する程度ならfixtureで十分かなという気持ち」
「この記事にあったconnascenceという言葉が気になりました↓」「コードの結合や癒着の度合いを表す専門用語みたい」「元記事には、名前のconnascence、位置のconnascence、意味のconnascenceがあると書かれてますね」
参考: 結合度の尺度「コナーセンス」とは何か - Qiita
🔗 マルチテナンシーとWebSocket
つっつきボイス:「AnyCable作者のVladimir Dementyevさん(Evil Martians)が運営しているanycable.ioのブログ記事です」「タイトルのvs.は、マルチテナンシーのアプリでAction CableやAnyCableを使うことを指しているようですね」
「マルチテナンシーのアプリでは、今どのテナントのスコープにいるかがとても重要になるんですよ」「なるほど」「最近以下のようなコードをよく見かけますよね?」「最近一般的なマルチテナントなアプリケーションだとこういうの多いですね」「マルチテナンシーではまさにset_current_tenant
のような書き方をします」
# 同記事より
class ApplicationController < ActionController::Base
before_action do
current_account = find_account_from_request_or_whatever
set_current_tenant(current_account)
end
end
「記事にもあるように、マルチテナンシーでAction Cableを使う場合もそういうものを扱う必要があります」「request.subdomain
でテナントを切り替えるのは、SlackやAtlassianのプロダクトのようなマルチテナントアプリでよく見かけますね」
# 同記事より
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user, :tenant
def connect
self.tenant = request.subdomain
Apartment::Tenant.switch!(tenant)
end
end
end
「Railsで普通使われる通常のHTTPリクエストの場合は、リクエストの中で使うコンテキストはそのリクエストの中で閉じることを前提にできるんですよ」「ふむふむ」「でもAction Cableの場合は1個のWebSocketを使い回すので、メッセージ間のコンテキストが分離されるとは限らない: つまりコンテキストを適切に切り替えないと事故につながります」「テナント同士の情報が混じったら非常にマズいですね」「漏洩事故ですね」
「おそらく一番ややこしいのは、1個のWebSocketの中でコンテキストを切り替えるときでしょうね: using_current_tenant { super }
の部分とかがそう↓」「あ〜なるほど」「こういうところで事故ると怖いので、個人的にはクライアント側がアクセスしたいテナントごとにWebSocketごと切り替える方がいいのではという気持ちがあります」「それもわかる」「記事はAction CableとAnyCableそれぞれについて書かれていますが、このあたりはどちらを使おうと同じ注意が必要ですね」「なるほど」
# 同記事より
module ApplicationCable
class Connection < ActionCable::Connection::Base
# ...
# Make all channel commands tenant-aware
def dispatch_websocket_message(*)
using_current_tenant { super }
end
# The same override for AnyCable, which uses a different method
def handle_channel_command(*)
using_current_tenant { super }
end
end
end
参考: WebSocket API (WebSockets) - Web API | MDN
🔗 Sprockets->Propshaft公式アップグレードガイド
つっつきボイス:「このガイドがあることに最近気づいたので近々翻訳を出します」「そういえば少し前からできていた覚えがあるかも」「こういうドキュメントが整備されてくるとやりやすくなりますね👍」
前編は以上です。
バックナンバー(2022年度第3四半期)
週刊Railsウォッチ: 6月のRubyコア動向、Stack Overflowアンケート結果ほか(20220705後編)
- 20220704前編 マイグレーションをStrategyパターンで拡張可能にほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)