Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: form_withのmodelオプションへのnil渡しが非推奨化、Dockerfileでjemallocが有効にほか(20240221前編)

こんにちは、hachi8833です。


つっつきボイス:「そういえば高校の情報IIは選択科目だったかな」「情報Iが必須でしたっけ」「国公立大学入試も最近は共通テストって言うみたいですね」「いろいろ変わってきているな〜」「教えられる先生どのぐらいいるのか心配...」

令和4年度より、新しい高等学校学習指導要領に基づき、高等学校情報科においては共通必履修科目「情報Ⅰ」が新設され、全ての生徒がプログラミングやネットワーク、データベースの基礎等について学習することとなります。
選択科目「情報Ⅱ」では、プログラミング等についてさらに発展的に学習することとなります。
高等学校学習指導要領 情報科関係資料:文部科学省より

つっつき後に、同科目の教科書および教員研修用教材と、公式学習動画を見つけました↓。

参考: 授業・研修用コンテンツ:文部科学省
参考: 高等学校情報科『情報Ⅰ』授業・研修用コンテンツ

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

🔗 form_withmodelオプションにnilを渡すことが非推奨化された

form_withメソッドにmodel:引数の値としてnilを渡すことを非推奨化する。

Collin Jilbert
同Changelogより

動機/背景

このプルリクを作成した理由は、form_withに渡すmodel:引数のデフォルト値を変更するにあたり、非推奨警告を最初に表示してからにしたいため。

詳細

このプルリクは、#49943で導入された変更をいったん削除する。これらの変更はbreaking changeなので、代わりに、最初にform_withmodel:引数の現在のデフォルト値を廃止する。
同PRより


つっつきボイス:「最初は#49943model:のデフォルト値を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?

Rails 5.1〜7.1: 'form_with' APIドキュメント(翻訳)


つっつき後に元の#49943を読んでみました。

動機/背景
最初に、お忙しい中この説明を読んでくれた皆さんに感謝を申し上げたい。

form_withメソッドにおいて(私の考える)有用な変更を提案したい。form_withmodel:引数に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_withmodel:引数に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_forcase文で設定されるローカル変数も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::Notificationssql.active_recordrow_countフィールドを追加

ActiveSupport::Notificationssql.active_recordrow_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の利用法に応じて呼び出しを各アダプタに移動した。

RecordFetchWarningexec_queriesにパッチを適用する形で機能するので、複数の結果セットのサイズを集計する。クエリは1つにまとまるが、Vitessはクエリごとに制限をかけるので、このケースではあまり役に立たない。さらに、コネクション上で生SQLを実行するケースはサポートされない(これはこのパッチがキャプチャする)。

ペイロードのrow_countというフィールド名は、instantiation.active_recordresult_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::Encryptorcompressオプションを追加する。デフォルトは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)」

basecamp/marginalia - GitHub

🔗 RailsのDockerfileでjemallocがデフォルトで使われるようになった

メモリ最適化のため、Dockerfileでjemallocをセットアップするようになった。

Matt Almeida, Jean Boussier
同Changelogより

Rubyがmallocを利用すると、特に複数のスレッドが使われる場合に、Pumaと同様のメモリ断片化問題が発生する可能性がある(参考)。異なるパターンを利用して断片化を回避するアロケーターに切り替えれば、メモリ使用量を大幅に削減できる。

同PRより

jemalloc/jemalloc - GitHub


つっつきボイス:「今の時代ならproduction用のDockerでRubyのjemallocをデフォルトで有効にしてもいいでしょうね👍」「jemallocといえばmallocよりも高性能なメモリアロケーションライブラリとして以下の記事↓でも紹介されていましたね」

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)


前編は以上です。

バックナンバー(2024年度第1四半期)

週刊Railsウォッチ: Turbo 8リリース、Railsドキュメント改善プロジェクトほか(20240215)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。