- Ruby / Rails関連
週刊Railsウォッチ: RailsからOpenStructを削除、Playwrightベストプラクティスほか(20240425前編)
こんにちは、hachi8833です。
- お知らせ: 来週および再来週の週刊Railsウォッチはお休みをいただき、通常記事を公開します。
🔗Rails: 先週の改修(Rails公式ニュースより)
This Week in Rails is out with a short and sweet update containing three fixes submitted by ClearlyClaire, @_byroot, and @fatkodima. Check them all out here: https://t.co/OfSEH0sPw7
— Ruby on Rails (@rails) April 12, 2024
🔗 特定のUNIONを含むクエリでSQL構文エラーが生成されてしまう問題を修正
従来のArelでは、
LIMIT
やORDER BY
を含むUNION
クエリもしくはUNION ALL
クエリをビルドすると無効なSQLが生成されていた。このプルリクでは、構文エラーを避けるためにUnion
ノードとUnionAll
ノードのSELECT
ステートメントが丸かっこ()
で囲まれるようにArel::Visitors::ToSql
を変更した。
同更新情報より
動機/背景
#40181で説明されているように、ArelでLIMIT
やORDER BY
を含むUNION
クエリもしくはUNION ALL
クエリをビルドすると、無効なSQLが生成される。現時点では、Arelを直接使わずにこのようなクエリをビルドできるとは思えないが、
LIMIT
を含む再帰的なCTEをビルドすることについては興味があり(詳しい文脈についてはmastodon/mastodon#29889を参照)、そこでの障害の1つが、Arelが無効なSQLを生成するというものだった。詳細
このプルリクでは、構文エラーを回避するために
Union
ノードやUnionAll
ノード内のSELECT
ステートメントを丸かっこ()
で囲むようArel::Visitors::ToSql
を変更する。そのために、既存の
Arel::Visitors::PostgreSQL#grouping_parentheses
を一般化してArel::Visitors::ToSql
に移動し、これによってinfix_value_with_paren
で使われるようになった。追加情報
自分はActive Recordの内部やサポートされているあらゆるバックエンドについて詳しいわけではないが、このソリューションについては割と自信がある。
結果のSQLに対する仮定が厳密だったため、既存のテストの一部を更新する必要があった。
同PRより
つっつきボイス:「Arelを使わなければビルドできそうにないクエリの修正なんですね」「そもそもLIMIT
やORDER BY
を含むUNION
クエリとかをArelで書くことってほとんどなさそうですし」「以下のようなクエリが丸かっこ()
で囲まれるようにするようになった↓、なるほど」
# activerecord/test/cases/arel/select_manager_test.rb#L354
sql = manager.to_sql
_(sql).must_be_like %{
WITH RECURSIVE "replies" AS (
- SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42
+ (SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42)
UNION
- SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id"
+ (SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id")
)
「Arelの修正なのでChangelogは書かなかったとありますね」「Arelは一部を除いて基本的にprivate APIですよね(ウォッチ20230405)」
🔗 SQLiteのALTER TABLEで仮想カラムが誤って通常のカラムとして複製されていたのを修正
修正: #51522
RailsでSQLiteをALTER TABLEすると、新規テーブルを作成してから古いテーブルの構造とデータを新しいテーブルにコピーする。
ここで問題なのは、仮想カラムが誤って通常のカラムとしてコピーされること。このプルリクではこれを修正する。
同PRより
つっつきボイス:「SQLiteの仮想カラムが誤って通常のカラムとしてコピーされていたのはたしかに修正が必要」「原文のclassic columnsって通常のカラムのことか」「テーブルの新規作成をSQLiteのALTER TABLEしたものにコピーする形で行っているんですね」
「ここで言うSQLite3の仮想カラム(virtual column)っていわゆるgeneratedなカラムのことでしょうか?」「SQLite3のドキュメントを見た感じでは、generatedカラムのうち生成結果を実際にファイルに保存されるのがSTORED
で、いわゆるfunctionalカラム的にその都度生成するのがVIRTUAL
に該当するということのようですね↓: 前者は生成結果を実際にファイルに保存するのでインデックスを設定できるし、後者はその都度生成するのでインデックスを設定できないことになる」「なるほど」
参考: SQLiteドキュメント Generated Columns
2.1.
VIRTUAL
カラムとSTORED
カラム
生成カラムは、VIRTUAL
またはSTORED
のいずれかになります。VIRTUAL
カラムの値は読み取り時に計算されますが、STORED
カラムの値は行の書き込み時に計算されます。STORED
カラムはデータベースファイル内のスペースを占有しますが、VIRTUAL
カラムは読み取り時にCPUサイクルを多く消費します。SQLの観点では、
STORED
カラムとVIRTUAL
カラムはほぼ同じであり、生成カラムのどちらの分類に対するクエリでも、同じ結果が生成されます。唯一の機能の違いは、ALTER TABLE ADD COLUMN
コマンドでは新しいSTORED
カラムを追加できないことです。ALTER TABLE
で追加できるのはVIRTUAL
カラムのみです。
Generated Columnsより
🔗 テストで自動先延ばしジョブのエンキューをトランザクション終了後でもチェックできるようになった
- PR: Fix
ActiveJob::EnqueueAfterTransactionCommit
API by casperisfine · Pull Request #51525 · rails/rails
修正: #51426(コメント)
perform_later
は、成功した場合はジョブのインスタンスを返し、エラーの場合はfalse
を返すことになっている。
enqueue
が自動遅延されると、実際のキューイングが成功するかどうかは当然予測不可能だが、後方互換性上の理由から、成功すると仮定するのがベスト。必要に応じてジョブインスタンスを維持し、トランザクション完了後に
#successfully_enqueued?
をチェックできるようにした。
同PRより
つっつきボイス:「この間から改修されているジョブエンキューの自動先延ばし(ウォッチ20240416)と関連している感じですね」「自動先延ばしされたジョブがトランザクション終了後にもエンキュー成功かどうかをチェックできるようにした: 後からチェックしたいというニーズはテスト以外にも当然あるのでこれは欲しいヤツ👍」
stub_const
にexists: false
パラメータを渡すことで、まだ存在していない定数をスタブできるようになった。Jean Boussier
同Changelogより
# activejob/test/cases/enqueue_after_transaction_commit_test.rb71
test "#perform_later returns the Job instance even if it's delayed by `after_all_transactions_commit`" do
fake_active_record = FakeActiveRecord.new(false)
stub_const(Object, :ActiveRecord, fake_active_record, exists: false) do
job = EnqueueAfterCommitJob.perform_later
assert_instance_of EnqueueAfterCommitJob, job
assert_predicate job, :successfully_enqueued?
en
「ところで、ジョブのエンキューまで自動で非同期にしたいかどうかというのは少し微妙な点があるかも: 非同期の振る舞いが増えるとややこしくなる一方ですし、たいていの場合はジョブエンジンで十分高速に処理できると信じていいはずなので、少なくともエンキュー部分は基本的に同期的にやりたい気持ちがある」「あ、たしかに!」「このプルリクで述べられているような状況を考えなければならないとしたらGitHubやShopifyぐらいの規模でしょうし、そういう場合は既に非同期エンキュー周りがだいぶややこしくなっていそう」「そういえばJean BoussierさんはShopifyの方ですね」「そのぐらいの規模でもなければエンキューをデフォルトで自動先延ばしすることはほぼないんじゃないかな」「たぶんないと思います」
🔗 Arelでサブクエリに渡される配列のプリペアド可能/不可を修正
動機/背景
このプルリクを作成した理由は、SQLのすべての
IN
ステートメントが自動的に「プリペアド不可」(プリペアドステートメントのこと)とマーキングされることに気づいたため。しかし、少なくともそのArelノードもプリペアド可能である限りはサブクエリもプリペアド可能であるべきに思える。この変更によって、プリペアド不可のArelノードのリストは以下のようになる。
SqlLiteral
: ここには何が含まれてもよいため。配列が渡される場合の
In
: 配列には任意の個数の項目やリテラル値を含められるため変動性が高く、プリペアドステートメントの候補としては不適切なため。配列が渡された場合の
NotIn
: 上のIn
と同じ理由。
HomogenousIn
: 配列だけを渡せる(72fd0ba)詳細
このプルリクは、Arelの
Nodes
のIn
とNotIn
を変更して、配列が渡された場合にのみpreparable = false
とマーキングするようにした。それ以外の場合はプリペアド可能なコレクタの状態を変更しない。Arelはpublic APIではないのでChangelogは作成しなかったが、リクエストがあれば喜んでそうするつもり。
同PRより
つっつきボイス:「プリペアドステートメントは以前も取り上げましたね(ウォッチ20220222)」「IN
ステートメントがプリペアド可能かどうかをArelが判定しているということか: こういうのがどのぐらい正しく動いているのかが少々気になりますね」
「従来は配列の要素型が異なる可能性があるので一律でプリペアド不可にしていたけど、subselectで要素型が揃っていればArelでHomogenousなInが使えるようにしたということらしい: 以下のQuoraを見る限りでは、homogeneousはデータセットの型が同じでheterogeneousはデータセットの型が異なっているとある↓」「72fd0baを見るとhomogeneousなら速くなるみたいですね」「要素型が違う可能性があったら実行時にその都度型チェックしないといけなくなりますけど、homogeneousなら省略できるので速くなりそう」
参考: What's the difference between homogeneous and heterogeneous data sets? - Quora
🔗 OpenStruct
の除去関連プルリク2件
#51491 (comment)の続き。
上のプルリクがマージされた後で、
require "ostruct"
がなくなったことによるエラーが1つあったので(CI)、単にOpenStructを使うことをやめればよさそう?
同PRより
ostruct
はjson
gemで暗黙にrequire
されるが、アップデートされたときにテストでOpenStruct
を初期化できずに失敗した。#51510で
ostruct
を削除しようとしていたが、今のCIが失敗しているので、つなぎとしてこれをプッシュしている。同PRより
つっつきボイス:「RubyのOpenStruct
を削除するプルリクと、それによるエラーを一時的に解消するプルリクがありました」「そもそもOpenStruct
は遅いし、OpenStruct
を使うぐらいなら普通にRubyのHash
を使う方がいいと思いますね」「なるほど、」「少なくともOpenStruct
を新規に使うのはやめた方がよいです」
参考: Ruby 3.0 から OpenStruct
が非推奨になった
参考: class OpenStruct
(Ruby 3.3 リファレンスマニュアル)
🔗 allow_browser
コンフィグでブラウザをブロックする場合は406でレスポンスするよう修正
参照: #50505
RFC 9110では以下のように規定されている。
サーバーは、必要なプロトコルを示すためにレスポンス426(Upgrade Required)でUpgradeヘッダーフィールドを送信しなければならない(MUST)。
https://httpwg.org/specs/rfc9110.html#status.426よりステータス406(Not Acceptable)は、リソースが以下であることを示せるので、より適切。
リクエストで受信したプロアクティブネゴシエーションヘッダーによると、user agentが受け取れる現在の表現がそのリソースに存在しない。
https://httpwg.org/specs/rfc9110.html#status.406よりプロアクティブネゴシエーションについては以下のように述べられている。
クライアントのネットワークアドレスやUser-Agentフィールドなどの暗黙的な特性。
https://httpwg.org/specs/rfc9110.html#proactive.negotiationより同PRより
つっつきボイス:「以前allow_browser
でブラウザの種類をRails側で制限できるようになっていましたね(ウォッチ20240123)」「RFCを丁寧に参照したうえで"426 Upgrade Required"レスポンスを受け取れないブラウザには"406 Not Acceptable"レスポンスを返すべきという修正、言われてみればたしかに」「406用のunsupported browserページも追加されていますね↓」
# railties/lib/rails/generators/rails/app/app_generator.rb#L495
def delete_public_files_if_api_option
if options[:api]
remove_file "public/404.html"
+ remove_file "public/406-unsupported-browser.html"
remove_file "public/422.html"
- remove_file "public/426.html"
remove_file "public/500.html"
remove_file "public/icon.png"
remove_file "public/icon.svg"
end
end
参考: 426 Upgrade Required - HTTP | MDN
参考: 406 Not Acceptable - HTTP | MDN
🔗Rails
🔗 Playwrightベストプラクティス
つっつきボイス:「Playwrightは以前取り上げたWebのE2Eテストフレームワークでしたね(ウォッチ20230906)」「"ユーザから見えるふるまいをテストする"、"テストはできるだけ分離する"など、その通りだと思います: システムテストはただでさえ遅いのでなるべく分離しておきたい」「"サードパーティの依存関係をテストしない"はものによっては難しいこともあるけどわかる」「お、記事で紹介されているPlaywright用のVS Code拡張↓はよさそう👍: こういうのは人間が手で書くよりツールを使いたいですね」
参考: Playwright Test for VSCode - Visual Studio Marketplace
🔗 RailsのI18nで訳文が見つからない場合の対応
つっつきボイス:「Railsの国際化機能を使ったときに訳文のキーがなくてもデフォルトではwarningが表示されない問題、あるある」「大きく育ったRailsアプリで訳文がない場合の洗い出しを、記事にもあるi18n-tasks gemとかで後から一気にやろうとすると大量の未定義項目が出たり、アルファベット順に並べ替えるdiffが大量に出たりして辛いことになるんですが、国際化を真面目にやろうとすると結局こういうものが必要になってくるんですよ」「そうそう」「このgemは、日本語版に訳文があるけど英語版にない場合にエラーにしてくれたり、訳文をキーでソートしたりできます👍」
参考: 6.2.1 I18n::MissingTranslationData
の処理方法をカスタマイズする -- Rails 国際化(I18n)API - Railsガイド
🔗 RubyCademyより
✨ RUBY TIPS ✨
Use the '-' modifier in strftime to remove any leading whitespace when dealing with single-digit days ✨💫#ruby #rubyonrails pic.twitter.com/iQviwMZGj1
— RubyCademy (@RubyCademy) April 15, 2024
つっつきボイス:「strftime
でdayの部分を%e
ではなくて%-e
のようにマイナス記号-
を付けるとdayが1桁のときにスペースを追加しなくなる、これ知らなかった」
#同ツイートより
require 'date'
date = Date.new(2042, 11, 04)
date.strftime('%e %B %Y') #=> " 4 November 2042"
date.strftime('%-e %B %Y') #=> "4 November 2042"
参考: Time#strftime
(Ruby 3.3 リファレンスマニュアル)
-
: 左寄せにする(0埋めや空白埋めを行わない)
Time#strftime
(Ruby 3.3 リファレンスマニュアル)より
前編は以上です。
バックナンバー(2024年度第2四半期)
週刊Railsウォッチ: Kamalはゲームチェンジャーになるか、Solid Queueで使われているfugitほか(20240423後編)
- 20240416前編 ジョブのエンキューをトランザクション完了時まで自動先延ばしほか
- 20240410後編 SeleniumでRubyの全クラスとモジュールにRBSが追加ほか
- 20240409前編 Rails公式の"rails-new"ツールでRailsプロジェクトをセットアップほか
- 20240402 solid_queueとmission_control-jobsが正式にRailsのgemに、Rubyの"チルド"文字列ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)