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

週刊Railsウォッチ: Rails 8に入るSolid Cacheほか(20240312前編)

こんにちは、hachi8833です。


つっつきボイス:「今年のRubyKaigi 2024はライブ中継ないのか〜」「Super Earlybirdはもう売り切れなんですね」

週刊Railsウォッチについて

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

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

お知らせ: 来週の週刊Railsウォッチはお休みをいただき、通常記事を公開いたします🙇

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

🔗 汎用のfixtureアクセサを追加

minitestと競合する可能性のあるfixture名に対応するため、汎用のfixtureアクセサを公開する。

assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name

Jean Boussier
同Changelogより

assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
assert_equal "Ruby on Rails", fixture(:web_sites, :rubyonrails).name

これはMetadataモデルを持っている某氏から自分に提供されたもの。fixtureアクセサはmetadataだが、Minitestに最近追加されたmetadataメソッドと競合する。

cc: @zk-1

同PRより


つっつきボイス:「たしかにfixture名のような自動生成される名前が既存のものとかぶるのはよくある話」「factoryなんかもそうですね」「fixture()で囲うことで名前衝突を回避できるようになるのはいいですね👍」

# activerecord/lib/active_record/test_fixtures.rb#L256
      def method_missing(method, ...)
-       if fs_name = fixture_sets[method.name]
-         access_fixture(fs_name, ...)
+       if fixture_sets.key?(method.name)
+         fixture(method, ...)
        else
          super
        end
      end
...
+     def fixture(fixture_set_name, *fixture_names)
+       if fs_name = fixture_sets[fixture_set_name.name]
+         access_fixture(fs_name, *fixture_names)
+       else
+         raise StandardError, "No fixture set named '#{fixture_set_name.inspect}'"
+       end
+     end

「minitestの以下の変更でMinitestモジュールにmetadataというメソッドが入ったのがたまたまぶつかっていたことでこの問題に気づいたのね↓」

参考: + Add metadata lazy accessor to Runnable / Result. (matteeyah) · minitest/minitest@de80282

# lib/minitest.rb#L460
    def metadata
      @metadata ||= {}
    end

minitest/minitest - GitHub

🔗 assert_initializerを追加

Rails::Generators::Testing::Assertions#assert_initializerを導入。

既存のinitializerジェネレータアクションを補完する。

assert_initializer "mail_interceptors.rb"

Steve Polito
同Changelogより


つっつきボイス:「assert_initializerは新しいアサーションですね」「config/initializers/ディレクトリの下に指定のイニシャライザが存在しているかとか中身をチェックしたりするアサーションということなのか」「内容はassert_fileのショートハンドというシンプルなものですね↓」「アプリのイニシャライザがいつの間にか書き換えられていないかどうかのチェックに使う感じなのかな」「Railsフレームワークのジェネレータテストでも今後使うかも?」

# railties/lib/rails/generators/testing/assertions.rb#L141
+       def assert_initializer(name, *contents)
+         assert_file("config/initializers/#{name}", *contents)
+       end

参考: § 5 イニシャライザファイルを使う -- Rails アプリケーションの設定項目 - Railsガイド
参考: Rails API assert_file -- Rails::Generators::Testing::Assertions

🔗 development/test環境でdefault_url_optionsをデフォルトで設定するよう修正

development環境とtest環境でaction_mailer.default_url_options値を設定する。

このコミットが入る前は、新規Railsアプリケーションのメーラーに*_path helperでビルドしたURLが含まれているとActionView::Template::Errorエラーが発生していた。

Steve Polito
同Changelogより


つっつきボイス:「default_url_optionsコンフィグが未設定だと、開発中にAction Mailerのテンプレートで*_urlヘルパーとかを書いたときにエラーになっちゃうので、この修正が欲しい気持ちわかる: 設定を自分で入れれば解決できるけど、そのひと手間を解消してくれるのは地味にありがたい👍」「Rails開発の長い人なら一度は経験しているでしょうね」「修正ではlocalhost:3000がdeveloper環境とtest環境のデフォルトになるんですね」

# railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt#L46
+
+  config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

参考: §4.5 default_url_options --Action Controller の概要 - Railsガイド

コントローラと異なり、メーラーのインスタンスは受信リクエストに関するコンテキストをまったく持たないので、:hostパラメータは自分で設定する必要がある。
config.action_mailer.default_url_options = { host: "www.example.com" }
railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt#L47より

🔗 主キーでない単一のカラムでModel.query_constraintsのエラーメッセージを修正

主キーでない単一のカラムでModel.query_constraintsをraiseされるのは期待通りだが、そのエラーメッセージが正しくなかった。適切なエラーメッセージを表示するよう修正した。

Joshua Young
同Changelogより


つっつきボイス:「単一主キー(いわゆるサロゲートキー)がないカラムでquery_constraintsがエラーをraiseするときの条件が適切でなかったのね↓」「複合主キー(composite primary key)を使っているときに起きる可能性があるエラーなんですね」

# activerecord/lib/active_record/reflection.rb#L786
        def derive_fk_query_constraints(foreign_key)
          primary_query_constraints = active_record.query_constraints_list
          owner_pk = active_record.primary_key

-         if primary_query_constraints.size != 2
+         if primary_query_constraints.size > 2
            raise ArgumentError, <<~MSG.squish
              The query constraints list on the `#{active_record}` model has more than 2
              attributes. Active Record is unable to derive the query constraints
              for the association. You need to explicitly define the query constraints
              for this association.
            MSG
          end

「本当はこのメッセージが表示されるべきだったということですね↓」

# activerecord/test/cases/associations_test.rb#L381
assert_equal "The query constraints on the `Sharded::BlogPost` model does not include the primary key so Active Record is unable to derive the foreign key constraints for the association. You need to explicitly define the query constraints for this association.", error.message

Sharded::BlogPostモデルのquery constraintsには主キーが含まれていないので、Active Recordはこの関連付けで外部キー制約を導出できない。この関連付けのquery constraintsを明示的に定義する必要がある。

🔗 RailsのテストスイートにあるBase.connectionwith_connectionlease_connectionに置き換えた

振る舞いをより適切に反映するため#lease_connectionによって置き換えられる。

ActiveRecord::Base.connectionも同様に非推奨化されるが、削除猶予期間や非推奨化警告の表示は行わない。

今後内部で使われることのないようにするため、Active Recordのテストスイート内部からBase.connectionを削除する。

一部の呼び出し側(callsites)ではwith_connectionを利用する形に変換され、他の呼び出し側の一部についてもよりシンプルな形でlease_connectionに移行された。このプルリクは、#50793に基づいて変換される呼び出し側リストとして使える。
同PRより


つっつきボイス:「これと次はプルリクリストから見繕ったものです」「前回見た#51083の続きということですね(ウォッチ20240228)」「修正量は多いけど、基本的にconnectionwith_connectionlease_connectionに置き換えている↓: 特定のコネクションでコネクションプールを直接使うのをやめて、トランザクション内で同じコネクションプールに再接続できるwith_connectionlease_connectionに変えたいというような話でしたね」

# actionmailbox/test/migrations_test.rb#L6
class ActionMailbox::MigrationsTest < ActiveSupport::TestCase
  setup do
    @original_verbose = ActiveRecord::Migration.verbose
    ActiveRecord::Migration.verbose = false

-   @connection = ActiveRecord::Base.connection
+   @connection = ActiveRecord::Base.lease_connection
    @original_options = Rails.configuration.generators.options.deep_dup
  end

「この修正で従来のBase.connectionは非推奨になったということだけど、プルリクメッセージにも書かれているように、Base.connectionを非推奨化サイクルに乗せて今後削除することはしないという点がポイントですね: Base.connectionはとても広範囲に使われている定番のメソッドなので、仮に非推奨化アラートを表示すると、ありとあらゆるgemからものすごい量のアラートが発せられることになるでしょうね」「そうなったらつらいヤツですね」

Base.connectionは今後も外部のgemなどでは引き続き使えるけど、Railsフレームワーク内からは呼び出さないようにした: これは最も適切な処置だと思います👍」

「非推奨化されるけどアラートも出さず今後削除されることもないというのはかなり珍しいパターンかも: これに気づかないまま使い続ける人もたくさんいそうですし、実際そういう対応でもいける内容なんでしょうね」


前方互換性維持のために残されているレガシー機能といえば、以下の記事で取り上げられているcontent_tagを思い出しました↓。こちらは非推奨ではありませんが。

Rails: 5.1以降のtagヘルパー記法はcontent_tagより便利

🔗 uncachedメソッドにdirtiesオプションを追加

このプルリクは、ActiveRecord::Base.uncachedActiveRecord::ConnectionAdapters::ConnectionPool#uncacheddirtiesオプションを追加する。

true(デフォルト)に設定すると、書き込みで現在のスレッドに属するすべてのクエリキャッシュがクリアされる。
falseに設定すると、影響を受けるコネクションプールへの書き込みでクエリキャッシュがクリアされなくなる。

これは、Solid Cacheでキャッシュ書き込みによってクエリキャッシュがクリアされないようにするために必要。

Donal McBreen
同Changelogより


つっつきボイス:「これもActiveRecord::Baseとかに関連しているので上の#51240と関連しているのかなと思ったら、Solid Cache向けの別の改修のようですね」

参考: Rails API uncached -- ActiveRecord::QueryCache::ClassMethods

🔗 Solid CacheもRails 8に入る

「この間からSolid Queueが何度も話題になっていますけど、DBをエンジンにしたクエリキャッシュであるSolid CacheもRails 8でデフォルトになる流れになっていますね↓」「Solid QueueとSolid Cacheの字面が似ててSolid Cacheの方に気づいてなかった...」「 データベースをキャッシュとして使うこと自体はキャッシュ用テーブルを作れば可能なので、そうした実装としてSolid Cacheを作ったということでしょうね」

rails/solid_cache - GitHub

参考: Solid Cache should be the default caching backend for Rails 8 · Issue #50443 · rails/rails

参考: Rails API ActiveRecord::ConnectionAdapters::QueryCache

「最近の動きを見ると、Rails 8ではrails newしたときにジョブキューやクエリキャッシュのためのインフラをさしあたって決めなくても済むような方向に進めている感じはしますね」「自分もそんな感じがしています」「前回話したように(ウォッチ20240306)、アプリケーションの負荷が大きいときはRDBの仕事をあまり増やしたくないけど、アプリを新規作成するときにジョブキューやクエリキャッシュをどれにするかみたいな、決める必要のある項目が減るというのは新規アプリ開発プロジェクトで嬉しいことであるのはたしか👍」

「Railsチュートリアルのような教育用途でもジョブキューやクエリキャッシュを学びやすくなりそうですね」「インフラのセットアップが減るのは教育目的でももちろん有用だと思いますが、おそらくDHHの頭にあるのは主に業務プロジェクトで使う新規アプリのセットアップ軽減と期間短縮なんじゃないかなと想像しています」「なるほど」

参考: Ruby on Rails チュートリアル:プロダクト開発の0→1を学ぼう

「なお、クエリキャッシュについてはファイルシステム上(FileStore)やメモリ上(MemoryStore)のキャッシュなら現状でも追加インフラなしでできますけど、異なる環境同士でキャッシュを共有できないという問題があるんですよ」「あ、そうでしたか」「ファイルベースのものはコンテナと相性があまりよくないし、メモリベースのものもコンテナに割り当てられるメモリはそんなに多くないので、そのまま本番のコンテナにデプロイしたときなんかにメモリ上のクエリキャッシュが増えるとメモリを使い切ってしまうといった懸念はありますね」「たしかに」「MemoryStoreはプロセスが違うと共有できませんし、FileStoreはコンテナ(ホスト)が違うと共有できませんけど、Solid Cacheはコンテナ間でも共有が可能な点が大きなメリットだと思います👍」

参考: Rails のキャッシュ機構 - Railsガイド


前編は以上です。

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

週刊Railsウォッチ: method_missingの引数を'...'に置き換え、JRuby Prism、Sidekiqのしくみほか(20240306)

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

Rails公式ニュース


CONTACT

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