- Ruby / Rails関連
週刊Railsウォッチ: Arel::Nodes::Cteが追加、html_escape_onceの修正ほか(20230613前編)
こんにちは、hachi8833です。ご存知かと思いますがリマインドです。
Rails 6.0系は6月1日にサポートが切れたようです。まだ使ってる人がいたら要注意〜!
> Rails 6.0.Zがサポート対象シリーズのリストに含まれるのは、2023年6月1日までです。https://t.co/LckDT9RfHe pic.twitter.com/5PFKdW9glp
— Junichi Ito (伊藤淳一) (@jnchito) June 5, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 機能追加・変更
🔗 Arel::Nodes::Cte
を追加
WITH
句で使うArel::Nodes::Cte
を追加する。
また、Cte
ノードではクエリプランナーに対して明示的にMATERIALIZED
またはNOT MATERIALIZED
ヒントも提供する。posts = Arel::Table.new(:posts) comments = Arel::Table.new(:comments) good_comments_query = comments.project(Arel.star).where(comments[:rating].gt(7)) cte = Arel::Nodes::Cte.new(:good_comments, good_comments_query, materialized: true) query = posts. project(Arel.star). with(cte). where(posts[:id].in(cte.to_table.project(:post_id))). puts query.to_sql # WITH "good_comments" AS MATERIALIZED (SELECT * FROM "comments" WHERE "comments"."rating" > 7) SELECT * FROM "posts" WHERE "posts"."id" IN (SELECT post_id FROM "good_comments")
Jon Zeppieri
同Changelogより
動機/背景
PostgreSQLとSQLiteはどちらもCTE構文の非標準拡張をサポートしており、CTEサブクエリをマテリアライズすべきである(つまりメインクエリに織り込まずに個別に評価する)ことを指定できる。これは、クエリプランナーの判断が適切でない場合に便利。どちらのデータベースでも
WITH foo AS MATERIALIZED (...) ...
という構文を使う。同様に、どちらのデータベースもCTEをマテリアライズしないヒントを
WITH foo AS NOT MATERIALIZED (...) ...
で明示的に与えられる。詳細
このプルリクはArel::Nodes::Cte
を追加する。これはCTEクエリを表し、エイリアスやリレーションとマテリアライズフラグで構成されている。後者のフラグがtrue
の場合はマテリアライズを指示し、false
の場合はマテリアライズしないことを指示する。nil
の場合はマテリアライズのヒントを出さない。このノードは以下のように利用できる。posts = Arel::Table.new(:posts) comments = Arel::Table.new(:comments) good_comments_query = comments.project(Arel.star).where(comments[:rating].gt(7)) cte = Arel::Nodes::Cte.new(:good_comments, good_comments_query, materialized: true) query = posts. project(Arel.star). with(cte). where(posts[:id].in(cte.to_table.project(:post_id))). puts query.to_sql # WITH "good_comments" AS MATERIALIZED (SELECT * FROM "comments" WHERE "comments"."rating" > 7) SELECT * FROM "posts" WHERE "posts"."id" IN (SELECT post_id FROM "good_comments")
As
とTableAlias
ノードは、引き続きSelectManager#with
の引数としてサポートされる。追加情報
これは#47951の代替案であり、
MATERIALIZED
の追加を表すだけのノードを追加するのではなく、@matthewdが提案した「エイリアスとリレーションを両方含むCte
ノードを追加する」案に従ったものである。これにより構文木が浅くなり、キーワード固有のノード種別をさらに追加したりノードのネストを深くするという最終手段に訴えなくてもNOT MATERIALIZED
ヒントを簡単にサポートできるようになる。
同PRより
つっつきボイス:「お、Arelの機能追加じゃないですか😋」「joins
でのCTEサポートウォッチ20230524に続くCTEの機能追加ですね」「PostgreSQLやSQLite3にはAS MATERIALIZED
っていう構文があるのか」「通常のクエリを()
で囲んでその後ろに書けるんですね」
参考: PostgreSQL 15ドキュメント 7.8. WITH問い合わせ(共通テーブル式)
参考: SQLite3ドキュメント The WITH
Clause
「この記事↓によるとPostgreSQLが自動的にマテリアライズする場合の条件が"同じCTEが2回以上使われていないこと"と"CTE中に非immutable関数が使われていないこと"とある: 同じCTEを何度も使うならマテリアライズして使い回す方がいいし、CTEで改変が行われたら結果が変わってしまうので、おっしゃる通り」
🔗 マルチプルDBで使えるActiveRecord.disconnect_all!
が追加
これは基本的に
ActiveRecord::Base.connection.disconnect!
のマルチDB版。データベースに既に接続していない場合の接続も回避する。
これは、establish_connection
を使った後でステートをリセットするのに便利。
同PRより
つっつきボイス:「お、マルチDBコンシャスのActiveRecord.disconnect_all!
が追加された」「再接続も防止するんですね」「古いデータベースサーバーをフェイルオーバーするときにコネクションを一括で切断したいようなときはまれにあるかもしれませんね: 普段使う機能ではなさそう」
🔗 rescue
可能な例外をtest環境のエラー画面に表示するようになった
action_dispatch.show_exceptions
を:all
、:rescuable
、:none
のいずれかに変更する。
all
と:none
は、それぞれ従来のtrue
とfalse
と同じ動作になる。
新しい:rescuable
オプションは、rescue可能な例外(例:ActiveRecord::RecordNotFound
)だけを表示する。test環境では:rescuable
がデフォルトになる。
Jon Dufresne
同Changelogより
背景
統合テストでは、アプリケーションが本番環境に近い応答をすることが望ましい。
これによってアプリケーションがその通りに動作することを確信できるようになる。Railsのテストでは、test環境とproduction環境にひとつ大きなミスマッチがある。HTTPリクエスト中に発生した例外(例:
ActiveRecord::RecordNotFound
)がrescueされずにテスト内で再発生し、404レスポンスに変換されてしまう。
config.action_dispatch.show_exceptions
をtrue
に設定すれば、test環境がproductionと同じように動作するようにはなる。しかし、予期しない内部サーバーエラーが発生した場合、テストが表示するのは有用なスタックトレースではなく、詳細のわからない500レスポンスになり、デバッグが難しくなる。このため開発者は、統合テストをもっと高品質にするか、失敗時のデバッグ体験の向上を優先するかという二択を迫られる。
自分は両方のいいとこ取りが可能であることを提案したい。
ソリューション
config.action_dispatch.show_exceptions
設定オプションを従来のブーリアンから:all
、:rescuable
、:none
の3つの値のいずれかに変更する。値:all
と:none
は、それぞれ従来のtrue
とfalse
と同じように動作する。従来のtrue
(現在は:all
)については、test以外の環境では引き続きデフォルト値となる。新しい値
:rescuable
は、test環境での新しいデフォルトとなる。ActionDispatch::ExceptionWrapper.rescue_responses
で定義されたrescue可能な例外に対してのみ、レスポンスで例外を表示するようになる。
予期しない内部サーバーエラーが発生した場合に有用なスタックトレースと良いデバッグ体験を提供するために、test環境ではエラーの原因となる例外を引き続きraiseする。
同PRより
つっつきボイス:「test環境でrescue
可能なエラー画面をもっと親切なものにする改修だそうです」「HTTPリクエスト中に発生した例外がrescue
されないで再発生して404 Not Foundに変換される現象があるとは知らなかった」「これで困った覚えはなかったけど、エラー表示が改善されるのはいいですね👍」「今までconfig.action_dispatch.show_exceptions
にはtrue
/false
しか設定できなかったのが、:all
/:rescuable
/:none
の3つになるのね」
参考: §3.5.8 ActionDispatch::ShowExceptions
-- Rails アプリケーションを設定する - Railsガイド
🔗 バグ修正
🔗 change_in_place?
の挙動を修正
#40383のフォローアップ
修正: #48255
修正: #48262バイナリカラムでシリアライズド属性がサポートされている場合は、
raw_old_value
とraw_new_value
が両方ともBinary::Data
にキャストされるようにしなければならない。さらに、
Binary::Data
はEncoding::BINARY
で背後の文字列をキャストしなければならない。そうしないとASCII範囲外のバイトを含む文字列の比較に失敗する。自分へのメモ: これはバックポートすべき。
同PRより
つっつきボイス:「Active Recordの型キャスト周りに不整合があったのを修正したらしい: 必要な場合にEncoding::BINARY
の値を使うようになったんですね↓」
# activemodel/lib/active_model/type/binary.rb#L38
class Data # :nodoc:
def initialize(value)
- @value = value.to_s
+ value = value.to_s
+ value = value.b unless value.encoding == Encoding::BINARY
+ @value = value
end
参考: PostgreSQL 15ドキュメント 8.4. バイナリ列データ型
🔗 ERB::Util.html_escape_once
のエスケープ済み文字がhtml_safe
でマーキングされていなかったのを修正
ERB::Util.html_escape_once
が常にhtml_safe
文字列を返すよう修正。従来このメソッドは、戻り値の文字列の
html_safe?
プロパティを保持していた。しかしこの文字列はエスケープ済みであるため、html_safe
とマーキングしないとエンティティが2回エスケープされることになる。以下はビュースニペット例
<p><%= html_escape_once("this & that & the other") %></p>
変更前は上が2回エスケープされて以下のようにレンダリングされていた。
変更後は以下のように正しくレンダリングされる。
修正: #48256
Mike Dalessio
同Changelogより
つっつきボイス:「html_escape_once
でエスケープしてもhtml_safe
でマーキングされていなかったので二重エスケープされていたのか」「フラグが合ってなかったんですね」「踏んだら悲しいバグ」
参考: Rails API html_escape_once
-- ERB::Util
🔗 Authorization
ヘッダーに空の値を渡すとエラーになる問題を修正
動機/背景
このプルリクを作成した理由は、特殊な形で区切られたAuthorization
ヘッダー値がArgumentError
の発生に利用される可能性があるため(多くの場合500エラーレスポンスが返される)。詳細
このプルリクは、token_and_options
メソッドを変更して引数エラーの原因となる空白値を除去する。
通常、raw_params
はタプルの配列を返すが、Authorization
ヘッダーがBearer foo,,bar
のようにブランク値を含む場合(2つの,
区切り文字の間の値が空欄である点に注意)、raw_params
メソッドにタプル以外の値が含まれることになる。
これらの値は次にHash[]
に渡されるが、このメソッドは引数を1個または2個しか受け取らず、0個の引数は受け取らない。残念ながらこれはArgumentError: invalid number of elements (0 for 1..2)
を発生する。
同PRより
つっつきボイス:「あ〜なるほど、Bearer foo,,bar
のように値のない項目を含むものをHTTPのAuthorization
ヘッダーに渡すとエラーになってたのか: これは必要な修正」
参考: Authorization
- HTTP | MDN
🔗 モデルのジェネレータにcreate_table_migration
テンプレートのオーバーライドが正しく反映されない問題を修正
create_table_migration
テンプレートをオーバーライドしてもActiveRecord::Generators::ModelGenerator
に正しく反映されないバグを修正。rails g model create_books title:string content:text
この修正によって、以下の場所で
create_table_migration.rb.tt
テンプレートから順序正しく読み込まれるようになる。lib/templates/active_record/model/create_table_migration.rb lib/templates/active_record/migration/create_table_migration.rb
Spencer Neste
同PRより
つっつきボイス:「これはジェネレータのバグ修正ですね」「ところでマイグレーションのジェネレータは今もよく使うけど、モデルやscaffoldのジェネレータはめったに使わないかな」「マイグレーションファイルの連番は人間が入力するものじゃないですよね」
参考: Active Record マイグレーション - Railsガイド
🔗 rails new
でJavaScriptとCSSのオプションの組み合わせによって起きるバグを修正
動機/背景
このプルリクを作成した理由は、現在のRailsのアプリジェネレータに2つ問題があるため。問題1
JavaScriptオプションにimportmap
を指定し、CSSオプションにbootstrap
、bulma
、またはpostcss
を指定すると、Railsはimportmap
の指定を無視してJavaScriptオプションで強制的にesbuild
を設定してしまう。JavaScriptにオプションに
importmap
を指定して、Node.jsでbootstrap
、bulma
またはpostcss
を指定しても問題はないはず。問題2
JavaScriptオプションに
importmap
を指定するとRailsが強制的にesbuild
にする場合(上と同じ問題)や、Node.jsが必要なCSSオプションを使う場合でも、.node-version
ファイルが作成されず、Dockerfile
にNode.jsインストール手順が含まれない。詳細
このプルリクでは、.node-version
ファイルが作成され、Dockerfile
にNode.jsのインストールが含まれるように要件を変更する。
また、従来の動作も変更され、CSSオプションの選択内容にかかわらずimportmap
を選択できるようになる。
1:jsbundling-rails
で必要なnode
がインストールされないままimportmap
からesbuild
に切り替わる
なお、まだ利用できない組み合わせがある。CSSをコンパイルするためにimportmap
とcssbundling-rails
を使いたい場合は、tailwindcss-rails
やdartsass-rails
ではなく、tailwind
やsass
を使うことになる。
同PRより
つっつきボイス:「Rails 7が出た頃に以下の記事↓を書いたんですが、rails new
のオプションの組み合わせがとてもややこしいと思っていたらバグがあったんですね」「複雑になるとバグも入りやすくなりがち」「組み合わせを網羅したマトリックスがメンテで必要になるだろうけど、相当大きくなりそう」「矛盾する組み合わせでオプションを指定したらどうなるか知りたいですね」
🔗 非推奨化
🔗 SafeBuffer#clone_empty
が非推奨化
動機/背景
SafeBuffer#clone_empty
はもうRailsのコードベースで使われていない。最後の呼び出し元も2014年(Rails 4.2.0)に479c7caで削除された。追加情報
グローバルにGitHub検索してみると、このメソッドがここ11年の間に書かれたり更新されたりしている頻度は高くなさそう。
今後もこのメソッドをサポートしたいだろうか?
同PRより
つっつきボイス:「SafeBuffer#clone_empty
は使ったことないけどSafeBuffer
自体はよく使いますね」「中身これだけだった↓」「SafeBuffer
はどこで使われているんでしたっけ?」「さっきのhtml_safe
フラグなどもSafeBuffer
が持っていますね: これはString
を継承しています」「なるほど」
# https://github.com/rails/rails/blob/e88857bbb9d4e1dd64555c34541301870de4a45b/activesupport/lib/active_support/core_ext/string/output_safety.rb#L210
def clone_empty
self[0, 0]
end
参考: Rails API ActiveSupport::SafeBuffer
🔗 assert_enqueued_email_with
で:args
引数へのHash渡しが非推奨化
#45752では、
assert_enqueued_email_with
に:params
が追加されて、メーラーのparamsやargsを同時に指定可能になった。しかし後方互換性のために、:args
のHashがparamとして温存された。その結果、:params
と:args
を両方指定し、かつ:args
がHashになっていると、:params
が無視されてしまう。このコミットは、
:args
でのparams指定(つまり:args
にHashを渡す)を非推奨化する。
この非推奨の振る舞いが削除された後は、:args
にHashを渡せるようになり、Hashが名前付き引数として扱われるようになる。
同PRより
assert_enqueued_email_with
に:args
キーワード引数経由でparamsを渡すことを非推奨化する。
これによって、assert_enqueued_email_with
が:params
キーワード引数をサポートするようになるので、paramsを渡すときはこれを使うこと。# 変更前 assert_enqueued_email_with MyMailer, :my_method, args: { my_param: "value" } # 変更後 assert_enqueued_email_with MyMailer, :my_method, params: { my_param: "value" }
メーラーの名前付き引数をHashとして指定するには、以下のようにHashを配列でラップすること。
assert_enqueued_email_with MyMailer, :my_method, args: [{ my_arg: "value" }] # または assert_enqueued_email_with MyMailer, :my_method, args: [my_arg: "value"]
Jonathan Hefner
同Changelogより
つっつきボイス:「assert_enqueued_email_with
はこの間も改修されていましたね(ウォッチ20230607)」
🔗 細かな改修
🔗 fixture_paths
にエンジンのtest/fixtures
を自動追加するようになった
パスがRailsアプリケーションのrootディレクトリに存在する場合は、
on_load
フックでエンジンのtest/fixtures
パスをfixture_paths
に追加するようになった。
Chris Salzberg
同Changelogより
動機/背景
イニシャライザで、test/fixtures
ディレクトリをデフォルトで自動的にTestFixtures#fixture_paths
に追加する。目的はエンジンのセットアップで大量の定型文を回避するため。詳細
#47675で、主にエンジンで使うためにfixtureのパスを複数割り当てるTestFixtures#fixture_paths
が導入された。
#47690ではそうしたパスを追加するフックも提供された。その結果、以下のようにエンジンで独自のfixtureパスを追加可能になった。
ActiveSupport.on_load(:active_record_fixtures) do self.fixture_paths << Engine.root.join("test", "fixtures").to_s end
しかしこのコードは書き忘れやすく、どのエンジンでも全く同じになる。そう考えれば、エンジンが自動的に test/fixtures を探し、もしディレクトリが存在すれば、このような形で
fixture_paths
に追加する方が理にかなっていそう。このプルリクはそれを実現する。
同PRより
つっつきボイス:「fixtureの改修: たしかにエンジンのfixtureもfixtureのパスに自動で入ってくれると嬉しいですね👍」
🔗 エラーのハイライトを修正
このコミットより前は、一部のレンダリング呼び出しでerror highlightが「利用できない」形でハードコードされていた。このため、正しいバージョンのerror highlightがインストールされているにもかかわらず、エラーページによって「error highlightをインストールする必要があります」というメッセージが表示されることがあった。
このコミットでは、
DebugView
クラスにデリゲートメソッドを追加し、デバッグ関連のテンプレートでerror highlightが利用可能かどうかをメソッド呼び出しで確認できるようにした。これにより、あらゆる場所でローカル変数の受け渡しに依存する必要がなくなる。
なおこの変更により、すべての「rescue」テンプレートがDebugView
クラスのコンテキスト内でレンダリングされなければならなくなるというマイナス面もある(しかし自分はそれでいいと思う)。
同PRより
つっつきボイス:「tenderloveさんによる改修です」「エラーのハイライトが効かない場合があったとは」「リファクタリングとあるけど実際は修正でしょうね」
🔗 RDocの固定フォント表示をシンプルな記法に変更
<tt>(\w+::\w+)</tt>
をすべて+$1+
に置き換える
例:<tt>ActiveRecord::Base</tt> -> +ActiveRecord::Base+
同PRより
つっつきボイス:「APIドキュメント表示のシンプルなリファクタリングですね」「変更範囲はさすがに多いですけど」
# actioncable/lib/action_cable/gem_version.rb#L3
module ActionCable
- # Returns the currently loaded version of Action Cable as a <tt>Gem::Version</tt>.
+ # Returns the currently loaded version of Action Cable as a +Gem::Version+.
🔗 after_bundle
ブロックがapp:template
コマンドでも実行されるようになった
アプリのテンプレートには
after_bundle blocks
が含まれている場合がある。これはテンプレートを実行した後、bundle install
を実行した後に実行する必要がある。gem "devise" after_bundle do generate "devise:install" end
このコミットより前は、
bundle install
とafter_bundle
ブロックはrails new
でテンプレートを適用するときしか実行されなかった。このコミットにより、bin/rails app:template
でもこれらを実行するようになった。
同PRより
つっつきボイス:「テンプレートにafter_bundle
というものを書けるのか↓」
# Railsガイドより
# template.rb
generate(:scaffold, "person name:string")
route "root to: 'people#index'"
rails_command("db:migrate")
after_bundle do
git :init
git add: "."
git commit: %Q{ -m 'Initial commit' }
end
参考: Rails アプリケーションのテンプレート - Railsガイド
🔗 yamlテンプレートに含めるドキュメントの改善
このプルリクを作成した理由は、訳文キーの値として
Yes
/No
を使い、訳語True
/False
を取得していたせいで、値をエスケープする必要があることに気づくまで30分ほど頭がおかしくなりそうになったため。このプルリクは、truthy/falsyの判断が難しいキーだけをエスケープする必要があるというコメントを含める形ですべてのen.ymlファイルを更新し、値についてもカバーするようにした。またこのプルリクでは、コメントの指示に合わせて二重引用符を一重引用符に変更した(式展開ではない文字列では一重引用符が良いので、Rubocopを怒らせないため)。
修正: #46475
同PRより
つっつきボイス:「yamlテンプレートに含まれている注意事項を更新したんですね↓」「そうそう、この間も話題にしましたけど(ウォッチ20230222)、ここに書かれているものは大文字小文字を問わずブーリアンとして解釈される点はyamlを扱うときに気をつけたいですね」
# actiontext/test/dummy/config/locales/en.yml#L22
+# Be aware that YAML interprets the following case-insensitive strings as
+# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
+# must be quoted to be interpreted as strings. For example:
「yamlで思い出したんですが、たとえばRailsのコンフィグを設定用のyamlに保存するときは、それぞれの設定の意味や対象がわかるようにyamlファイルのコメントにも情報を残して欲しいですね」「そうしておかないと他の人が見た時に設定をどう変えたらいいのかがわからなくなってしまいますよね」「特にパスの設定には、末尾に/
を書いていいのかいけないのかという情報もぜひ欲しい」「たしかに」
🔗 MemoryStore::DupCoder
のマーシャル呼び出しを削減して高速化
このコミットより前の
MemoryStore::DupCoder
は、dump
とload
の両方でEntry#dup_value!
を呼び出し、キャッシュされた値の外部改変を防止していた。Entry#dup_value!
は複雑なオブジェクトを複製するためにMarshal.dump
を呼び出して次にMarshal.load
を呼び出している。しかし外部からの改変を防ぐのであれば、DupCoder.dump
に対して一度だけMarshal.dump
を呼び、DupCoder.load
に対して一度だけMarshal.load
を呼べば済む。このコミットでは、
DupCoder
がEntry#dup_value!
に依存するのではなく、Marshal.dump
とMarshal.load
を直接呼び出すように変更し、複雑なオブジェクトの読み書きでMarshal
の処理を半減させる。
同PRより
つっつきボイス:「マーシャルの二重呼び出しを解消するという比較的シンプルな最適化ですね」
参考: Rails API ActiveSupport::Cache::MemoryStore
参考: module Marshal
(Ruby 3.2 リファレンスマニュアル)
前編は以上です。
バックナンバー(2023年度第2四半期)
週刊Railsウォッチ: Jets v4リリース、頑張らない型導入、Rust言語からCrabがforkほか(20230608後編)
- 20230607前編 MessagePackがcookieシリアライザとメッセージシリアライザにも導入ほか
- 20230531後編Rubyで環境変数を扱う、Web標準に「Baseline」ステータス追加ほか
- 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ウォッチタグ)