- Ruby / Rails関連
週刊Railsウォッチ: form_withのmodelオプションへのnil渡しが非推奨化、Dockerfileでjemallocが有効にほか(20240221前編)
こんにちは、hachi8833です。
情報IIの教科書、普通にscikit-learnで機械学習してるし、クラスタリングしてるし、SQLite3でRDBMSしているし、やばいんですよ https://t.co/AQ9oXPHVU5 pic.twitter.com/XsDutAhLC8
— ところてん (@tokoroten) February 14, 2024
面白いからみんなも斜め読みするといいよhttps://t.co/x2fuiVwBXH
— nishio hirokazu (@nishio) February 15, 2024
つっつきボイス:「そういえば高校の情報IIは選択科目だったかな」「情報Iが必須でしたっけ」「国公立大学入試も最近は共通テストって言うみたいですね」「いろいろ変わってきているな〜」「教えられる先生どのぐらいいるのか心配...」
令和4年度より、新しい高等学校学習指導要領に基づき、高等学校情報科においては共通必履修科目「情報Ⅰ」が新設され、全ての生徒がプログラミングやネットワーク、データベースの基礎等について学習することとなります。
選択科目「情報Ⅱ」では、プログラミング等についてさらに発展的に学習することとなります。
高等学校学習指導要領 情報科関係資料:文部科学省より
つっつき後に、同科目の教科書および教員研修用教材と、公式学習動画を見つけました↓。
参考: 授業・研修用コンテンツ:文部科学省
参考: 高等学校情報科『情報Ⅰ』授業・研修用コンテンツ
🔗Rails: 先週の改修(Rails公式ニュースより)
- 公式更新情報: Ruby on Rails — Deprecation, bugfixes and more!
- 公式更新情報: Ruby on Rails — Rails Foundation Docs, Query Logs source_location, Dockerfile gets jemalloc
🔗 form_with
のmodel
オプションにnil
を渡すことが非推奨化された
form_with
メソッドにmodel:
引数の値としてnil
を渡すことを非推奨化する。Collin Jilbert
同Changelogより
動機/背景
このプルリクを作成した理由は、
form_with
に渡すmodel:
引数のデフォルト値を変更するにあたり、非推奨警告を最初に表示してからにしたいため。詳細
このプルリクは、#49943で導入された変更をいったん削除する。これらの変更はbreaking changeなので、代わりに、最初に
form_with
のmodel:
引数の現在のデフォルト値を廃止する。
同PRより
つっつきボイス:「最初は#49943でmodel:
のデフォルト値をfalse
に変えることで、nil
を渡したらArgumentError
を発生するようにしていたけど↓、その改修をいったん取り消してまずは非推奨警告を出すようにしたという流れなんですね」「form_with
でモデルを指定せずに済ませたい場合はたまにあるけど、nil
よりはfalse
でやる方がいいでしょうね」
# https://github.com/rails/rails/pull/49943
# actionview/lib/action_view/helpers/form_helper.rb#L756
- def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
+ def form_with(model: false, scope: nil, url: nil, format: nil, **options, &block)
raise ArgumentError, "The :model argument cannot be nil" if model.nil?
つっつき後に元の#49943を読んでみました。
動機/背景
最初に、お忙しい中この説明を読んでくれた皆さんに感謝を申し上げたい。
form_with
メソッドにおいて(私の考える)有用な変更を提案したい。form_with
のmodel:
引数にnil
オブジェクトを渡すときに発生する可能性のある問題をデバッグしやすくする。自分自身もこの問題に何度か遭遇し、他の人も同様に遭遇していることがわかってきた。直近遭遇した問題は、自分や他の人がRailsの学習を手伝っている新しいRails開発者から寄せられたものだった。
この問題の発生シナリオは実際には2通りある。
- 1: indexページ上のフォームで
:model
引数にnil
が渡されると、ActionController::ParameterMissing
エラーが発生する。2: 新規ページでは
:model
引数にデフォルトでnil
が渡される。
このシナリオではActionController::RoutingError (No route matches [POST] "/posts/new")
が発生する(モデルがPost
の場合)。このシナリオでは、RailsがURLを現在のページからビルドすることにフォールバックすることが原因。ここではシナリオ1について、(可能なら)この問題を誰でも深く理解できるよう、問題発生までの流れを説明したい。
ジュニア開発者「strong parametersで問題が発生しました。Rails 7でstrong parametersの使い方が変わったんでしょうか?」
def url_link_params params.require(:url_link).permit(:url) end
「ちなみにモデル名は
UrlLink
です。フォームを送信しようとすると以下のエラーが発生しました」ActionController::ParameterMissing (param is missing or the value is empty: url_link)
「この
require
を削除してparams.permit(:url)
だけにすると、フォームを送信できました。でもform_with(model: url_link)
でフォームを送信するとurl
入力フィールドがform.url_field :url
になってわけがわかりません」私「お、コントローラのアクションで宣言されているインスタンス変数
@link
と、フォームのパーシャルでlocals
として渡されるときのインスタンス変数名@url_link
が食い違っていますね」def index @link = UrlLink.new end <%= render partial: "form", locals: { url_link: @url_link } %>
ジュニア開発者「ありがとうございます。変数名のミスマッチを修正したらフォームとコントローラが正常に動くようになりました」
form_with
でmodel:
引数にnil
オブジェクトを渡したときにActionController::ParameterMissing
エラーではなく、strong parametersに問題がある(自分は薄々そうだろうと思っていた)ことがわかるArgumentError
が発生するようになっていれば、このジュニア開発者のデバッグエクスペリエンスはもっと良いものになっただろう。詳細
このプルリクは、
form_with
メソッドのmodel:
引数のデフォルト値をnil
からfalse
に変更することで、nil
をこの引数に渡したらArgumentError
が発生するようにする。def form_with(model: false, other_args) raise ArgumentError, "Form model object is nil" if model.nil? end
model:
引数のデフォルト値としてfalse
よりもnil
がよいとされていた理由を考えてみたものの、何も思いつかなかった(心当たりがあれば知らせてもらえると嬉しい)。また、
form_for
のcase
文で設定されるローカル変数もnil
からfalse
に変更する。これは、form_for
から引き続きform_with
を正常に呼び出すために必要。追加情報
読んでくれた皆さんに感謝申し上げたい。お気づきの点のフィードバックも歓迎する。
#49943より
🔗 API生成時にCSSファイルを生成しないよう修正
動機/背景
このプルリクを作成した理由は、API生成でTailwind CSSも含まれてしまうため。
詳細
このプルリクは、APIでのCSSスキップの振る舞いを変更する。
追加情報
これによって#50900も部分的に修正される。
同PRより
つっつきボイス:「わかりやすいバグ」「修正もめちゃくちゃシンプルですね↓」「Railsがサポートするオプションの組み合わせが多いので、こういうのが残っていたりするのもわかる」
# railties/lib/rails/generators/app_base.rb#L601
def css_gemfile_entry
+ return if options[:api]
return unless options[:css]
...
🔗 .railsrcファイルのコメントアウトが効いていなかったのを修正
.railsrcファイル内でコメントアウトされた行は、
rails new generator
コマンドで引数として扱うべきではない。#
以降のテキストを無視するようにARGVScrubber
を更新する。Willian Tenfen
同Changelogより
つっつきボイス:「.railsrcってそういえばありましたね」「rails new
するときによく使うオプションを設定するファイルなので、rails new
するとき以外は影響ないヤツですね」「昔設定してみた覚えあったかも」
# railties/lib/rails/generators/rails/app/app_generator.rb#L660
def read_rc_file(railsrc)
- extra_args = File.readlines(railsrc).flat_map(&:split)
+ extra_args = File.readlines(railsrc).flat_map.each { |line| line.split("#", 2).first.split }
puts "Using #{extra_args.join(" ")} from #{railsrc}"
extra_args
end
rails new
のデフォルトオプションを~/.railsrcで設定できるようになりました。rails new
を実行するたびに利用するコマンドラインオプションをホームディレクトリの.railsrc設定ファイルで指定できます。
Ruby on Rails 3.2 リリースノート - Railsガイドより
🔗 ActiveSupport::Notifications
のsql.active_record
にrow_count
フィールドを追加
ActiveSupport::Notifications
のsql.active_record
にrow_count
フィールドを追加する。このフィールドは、通知を発行したクエリによって返された行数を返す。
このメトリクスは、大きな結果セットを含むクエリを検出したい場合に有用。
Marvin Bitterlich
同Changelogより
動機/背景
Vitessベースのデータベース(自分たちの場合はPlanetscaleなど)は、(デフォルトで)100kを超える結果セット行を返すクエリをブロックするため、Intercomでは大規模な結果セットを含むクエリの検出に取り組んでいる。
自分たちのところではうまく機能する内部パッチがあるが、他の人も恩恵を受けられるように、これをアップストリームに提供することを考えている。
詳細
このプルリクは
{*}Adapter.log
を変更してsql.active_record
通知を発行する。以前のInstrumentationはリクエストに含まれる情報のみをログ出力していたが、ブロック内の呼び出し元がペイロードを変更できるようになっていた。これを各アダプター内で利用して、各クエリで返される結果の行数を含む新しいフィールドを個別の
sql.active_record
通知に追加する。これにより、この通知 (例)の利用者がメトリクスを利用可能になる。
これに関する以前の作業として、
include
可能なRecordFetchWarning
モジュールがあり、これは大規模な結果セットを含むクエリを監視する必要性を示している。自分たちの場合、これらのログ行をobservabilityスタックに統合するのは簡単ではないが、個別のSQLトレースに数値を付加することで、observabilityスイートの能力を最大限に活用可能になった。このプルリクのInstrumentationテストスイートには、「モデルのインスタンス化クエリ」「
pluck
などの直接値クエリ」「生のActiveRecord::Base.connection.execute(sql)
クエリ」で機能することを検証する3つのテストも含まれる。パフォーマンス
この変更により、結果セットに要素がいくつあるかを問い合わせる。expectationではルックアップを常に行うべきとなっており、ベンチマークはこれが真実であることを示唆している。
この変更を
sqlite3
アダプタ、mysql2
アダプタ、postgresql
アダプタでexamples/performance
をテストしたが、パフォーマンスに測定可能な違いは見られなかった(標準偏差1%の場合、0.1%未満)。追加情報
当初は
abstract_adapter
内で行っていたが、結果で使われるメソッドがすべてのアダプタで異なっていることが判明したため、log
の利用法に応じて呼び出しを各アダプタに移動した。
RecordFetchWarning
はexec_queries
にパッチを適用する形で機能するので、複数の結果セットのサイズを集計する。クエリは1つにまとまるが、Vitessはクエリごとに制限をかけるので、このケースではあまり役に立たない。さらに、コネクション上で生SQLを実行するケースはサポートされない(これはこのパッチがキャプチャする)。ペイロードの
row_count
というフィールド名は、instantiation.active_record
のresult_count
と同じパターンに従っているため、良い命名と思える。
同PRより
つっつきボイス:「プルリクではかなり詳しく説明されていますね」「RailsのInstrumentation(計測)でrow_count
を取れるようになるとたしかに便利👍」
参考: Active Support Instrumentation で計測 - Railsガイド
🔗 ActiveRecord::Encryption
の暗号化で圧縮をオフにできるようになった
圧縮を無効にするオプションを
ActiveRecord::Encryption::Encryptor
に追加する。
compress: false
を設定することで、圧縮を無効にできる。class User encrypts :name, encryptor: ActiveRecord::Encryption::Encryptor.new(compress: false) end
Donal McBreen
同Changelogより
動機/背景
以下のような理由で、Active Recordの暗号化機能で圧縮を避けたい場合がある。
- データが既に圧縮済みの場合
- 暗号化された値のエントロピーに関する情報漏洩を避けたい(参考)
詳細
ActiveRecord::Encryption::Encryptor
にcompress
オプションを追加する。デフォルトはtrue
で、false
の場合はデータを決して圧縮しなくなる。これはモデル内の
encryptor
オプションで利用可能。class Record < ApplicationRecord encrypts :field, encryptor: ActiveRecord::Encryption::Encryptor.new(compress: false) end
同PRより
つっつきボイス:「たしかに圧縮済みデータを暗号化でさらに圧縮したくないですね」「暗号化された値のエントロピー(予測不可能性に関連する)が圧縮で若干下がる可能性も懸念しているらしい」
参考: Active Record と暗号化 - Railsガイド
🔗 Mysql2Adapter#active?
とTrilogyAdapter#active?
が正しく同期するよう修正
Mysql2Adapter#active?
とTrilogyAdapter#active?
が正しく同期するよう修正。
disconnect!
とverify!
についても同様。コネクションはスレッド間で共有されないはずなので、これは一般に大きな問題ではないが、トランザクションテストやシステムテストを実行する場合にはこれが要求されるため、SEGVが発生する可能性がある。
Jean Boussier
同Changelogより
つっつきボイス:「これもバグ修正」「トランザクションテストやシステムテストではアダプタをまとめて使うことがあるので問題になるのはわかる: synchronize
を追加することで同期するようにしたんですね↓」
# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L723
def disconnect!
- clear_cache!(new_connection: true)
- reset_transaction
- @raw_connection_dirty = false
+ @lock.synchronize do
+ clear_cache!(new_connection: true)
+ reset_transaction
+ @raw_connection_dirty = false
+ end
end
🔗 なかなか終わらないテストでテスト名を出力できるようになった
これは、テストを呼び出す前にMinitest によって呼び出され、テスト名を事前に出力できる。
これは、verbose(冗長)モードをオンにして、遅いテストがスタックするのをデバッグするのに有用。これにより、プロセスがデッドロックする前にスタックしたテスト名が出力される。
この機能がなければ、どのテストが返されないかを特定するためにダーティトリックに頼るしかなくなる。
デフォルトのMinitestレポーターは、verboseモードでこのように動作する。
同PRより
つっつきボイス:「なるほど、テストは基本的にランダムな順序で実行されるし、途中で止めてもテスト名がわからないと困るので改修したんですね」「こういうときにテスト名が欲しくなるのわかる: 地味に嬉しい機能👍」
🔗 ActiveStorage::Filename
のバグ修正
動機/背景
ActiveStorage::Filename
のエンコード時に引用符が落ちていたため、以下のように無効なJSONが生成されていた。JSON.generate(foo: ActiveStorage::Filename.new("bar.pdf") # => '{"foo":bar.pdf}'
詳細
to_json
を削除し、ActiveSupport::ToJsonWithActiveSupportEncoder
の実装に依存する形にした。
同PRより
つっつきボイス:「JSONの引用符がこういうふうに落ちるとエラーになりますね」「修正はto_json
の再定義を削除しただけなのね↓」
# activestorage/app/models/active_storage/filename.rb#L71
- def to_json
- to_s
- end
🔗 クエリログで:source_location
をサポート
クエリログのタグで
:source_location
タグオプションをサポート。config.active_record.query_log_tags << :source_location
呼び出し元の計算はコストの高い操作なので、基本的にdevelopment環境で使う(同じ目的を果たす
config.active_record.verbose_query_logs
もあることに注意)べき。production環境でデバッグ目的に使う場合は短期間にとどめるべき。fatkodima
同Changelogより
#42240のフォローアップ(#42240のコメントの議論に基づく)。
QueryLogs
で:line
オプションを利用できなくなった (#42240のコメント)が、Marginalia gemにはある。以前はこのgemで使うとコストがかかったが、basecamp/marginalia#138で高速化したので、従来ほどコスト高ではなくなり、使うかどうかは各自が決められるようになった。この
:line
オプション(更新情報: その後:source_location
という名前に決定された)を使いたい。これは非常に便利であり、これがないと、たとえば、DBクエリログが遅い場合に、クエリがコードベースのどの部分から生成されたかを突き止めるのが難しくなる。:action
コンポーネントがあっても、クエリがトリガーされた場所を見つけるために慣れないコードベースを調査する必要がある。
同PRより
つっつきボイス:「クエリログでソースコードの場所を:source_location
で出力可能になるのは結構便利そう👍」「ありがたい🙏」「処理が重いから必要なときだけ使う感じ」「そういえばQueryLogs
は以前37signalsのMarginalia gemから取り込んだ機能でしたね(ウォッチ20210906)」
🔗 RailsのDockerfileでjemallocがデフォルトで使われるようになった
メモリ最適化のため、Dockerfileでjemallocをセットアップするようになった。
Matt Almeida, Jean Boussier
同Changelogより
Rubyが
malloc
を利用すると、特に複数のスレッドが使われる場合に、Pumaと同様のメモリ断片化問題が発生する可能性がある(参考)。異なるパターンを利用して断片化を回避するアロケーターに切り替えれば、メモリ使用量を大幅に削減できる。同PRより
つっつきボイス:「今の時代ならproduction用のDockerでRubyのjemallocをデフォルトで有効にしてもいいでしょうね👍」「jemallocといえばmallocよりも高性能なメモリアロケーションライブラリとして以下の記事↓でも紹介されていましたね」
前編は以上です。
バックナンバー(2024年度第1四半期)
- 20240207後編 aws-sdk-rubyの全gemにRBSファイルが追加ほか
- 20240206前編 Pumaのデフォルトスレッド数変更、Rails 1.0をRuby 3.3で動かすほ
- 20240125後編 RailsコントローラのparamsはHashではない、ruby-enumほか
- 20240123前編 Railsの必須Rubyバージョンが3.1.0以上に変更ほか
- 20240119後編 Ruby 3.3でYJITを有効にすべき理由、Turbo 8の注意点8つほか
- 20240117前編 Rails 8マイルストーン、2023年のRails振り返り、Solid Queueほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)