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

週刊Railsウォッチ: Rails 7.1.0リリース、YARPがprismにリネームほか(20231011)

こんにちは、hachi8833です。先週のつっつきの直前に7.1が正式リリースされました🎉。

週刊Railsウォッチについて

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

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

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

Rails 7.1に含まれる最終組のプルリクたちです。

🔗 ログをブロードキャストするpublic APIを追加

背景

#44695を手がけていたときに、広く使われているBroadcastingがまだprivate APIのままであることに気づいた。@rafaelfrancaによれば、public APIにするなら元の実装を理解しやすくメンテナンスしやすくするためのリファクタリングが必要とのこと。

ブロードキャスティング

ブロードキャスティングは、要するに既存のロガーを「変換」してブロードキャストされたものにする形で動作する。
その後、ロガーは自身のメッセージをログに出力してフォーマットし、他のロガーにも伝える役目を担う。

この方法の問題は以下のとおり。

  • メタプログラミングを多用する。

  • ブロードキャスト内でロガーにアクセスすることも、ブロードキャストからロガーを削除することもできない。

  • さらに重要なのは、メインのロガー(他の部分にログをブロードキャストする)を変更できなかったこと。主にこの点が誤解の元になっていた。

  logger = Logger.new(STDOUT)
  stderr_logger = Logger.new(STDER))
  logger.extend(AS::Logger.broadcast(stderr_logger))

  logger.level = DEBUG # これは「他のすべてのロガー」のレベルを変更する
  logger.formatter = ... # 他のすべてのロガーでフォーマッタが変更される

Rails.loggerの戻り値を変更しないようにするため、新しいBroadcastLoggerクラスでは素のRuby Loggerクラスと同じメソッドを持つダックタイピングを実装する。
これはシンプルで退屈なPOROであり、ブロードキャストに含まれるすべてのロガーの配列を保持し、ログが送信されるたびにイテレーションする。

これで、ユーザーはブロードキャスト内ですべてのロガーにアクセスし、リアルタイムでそれらを変更可能になる。また、ブロードキャストから任意のロガーをいつでも削除できるようになる。

# 改修前
stdout_logger = Logger.new(STDOUT)
stderr_logger = Logger.new(STDER)
file_logger = Logger.new("development.log")
stdout_logger.extend(AS::Logger.broadcast(stderr_logger))
stdout_logger.extend(AS::Logger.broadcast(file_logger))
# 改修後
broadcast = BroadcastLogger.new(stdout_logger, stderr_logger, file_logger)

ブロードキャストの責務は、すべてをブロードキャスト内のロガーに渡すことだけであることがユーザーにとってもっと明確になるべきだとも思っている。そうなれば、broadcast.level = DEBUGを呼び出したときにブロードキャスト内のすべてのロガーでレベルが変更されることに驚かなくて済むようになる。

また、ブロードキャストのログを別の場所にブロードキャストする(broadcast.broadcast_to(stdout_logger, other_broadcast))といった、より複雑なセットアップも理解しやすくなる。
同PRより


つっつきボイス:「これは先週手短に取り上げたBroadcastLoggerですね(ウォッチ20231004)」「ログを複数の場所にブロードキャストする機能は今もあったような気がしますね🤔」「そのあたりの機能を柔軟に作り替えて、ブロードキャストのログをさらにブロードキャストするみたいなこともやりやすくなったそうです」

参考: ActiveSupport::BroadcastLogger - Ruby on Rails API


つっつき後に、「この機能名は振る舞いから言ってBroadcastよりもMulticastの方が適切なのではないか」という指摘がありました。

参考: マルチキャスト - Wikipedia

🔗 has_one_attachedにFileやPathnameをアタッチできるようになった

この変更が必要な理由
テストでモデルを作成するときに、(たとえばfile_fixtureで)FileやPathnameをModel.createに渡すといったことがやりやすくなる。

問題の解決方法
attachableをアタッチするときに、FileまたはPathnameかどうかをチェックして適切に処理する。
同PRより


つっつきボイス:「これはActive Storageの改修ですね: こういう感じでテストのモデルに添付ファイルを手軽に渡せるようになったのはいい👍」「FileはRuby標準ライブラリのクラスで、Pathnameはdefault gemなんですね」

# 同PRより
User.create!(avatar: File.open("image.jpg"))
User.create!(avatar: file_fixture("image.jpg"))

参考: class File (Ruby 3.2 リファレンスマニュアル)
参考: class Pathname (Ruby 3.2 リファレンスマニュアル)
参考: standard librariesとdefault gemsとbundled gemsの違い - ESM アジャイル事業部 開発者ブログ

🔗 register_parserを追加

動機/背景

指定のMIMEタイプでレンダリングされたコンテンツをデコードするcallableを登録する。

登録された個別のパーサーは、#rendered.$MIMEというヘルパーメソッドも定義する($MIMEmime引数の値に対応する)。

詳細

引数:

  • mime: レンダリングされたコンテンツのMIMEタイプ名のシンボル
  • callable: Stringをデコードするcallable、String値だけを引数として受け取る
  • block: callableを省略すると、ブロックがパーサーとなる

ActionView::TestCaseでは、デフォルトで以下のパーサーが定義される。

  • :html: Nokogiri::XML::Nodeのインスタンスを返す
  • :json: ActiveSupport::HashWithIndifferentAccessのインスタンスを返す

事前登録済みのパーサーには、対応するヘルパーも定義されている。

  • :html: rendered.htmlを定義する
  • :json: rendered.jsonを定義する

例:

レンダリングされたコンテンツをパースしてRSSにするには、RSS::Parser.parseへの呼び出しを登録する。

register_parser :rss, -> rendered { RSS::Parser.parse(rendered) }

test "renders RSS" do
  article = Article.create!(title: "Hello, world")

  render formats: :rss, partial: article

  assert_equal "Hello, world", rendered.rss.items.last.title
end

レンダリングされたコンテンツをパースしてCapybara::Simple::Nodeにするには、以下のようにCapybara.stringを呼び出して:htmlパーサーを再登録する。

register_parser :html, -> rendered { Capybara.string(rendered) }

test "renders HTML" do
  article = Article.create!(title: "Hello, world")

  render partial: article

  rendered.html.assert_css "h1", text: "Hello, world"
end

後方互換性のため、document_root_elementの既存サポートをrendered.htmlに基づいて再定義した。

追加情報

この提案は、ActionDispatch::Testing::RequestEncoderと、そのresponse.parsed_bodyテストメソッドからヒントを得ている。
同PRより


つっつきボイス:「これはテストへの機能追加かな」「レンダリングされたデータを解析できるパーサーをregister_parserで登録するとテストで使えるようになるそうです」「テストで欲しいパーサーを差し替えられるのか: 中身をこじあけて正規表現で取り出したりしなくて済むのはよさそう👍」「HTMLをCapybara.stringで解析したりRSSをRSS::Parser.parseで解析したりできるんですね」

参考: register_parser -- ActionView::TestCase::Behavior::ClassMethods - Ruby on Rails API

🔗 tagcontent_tagに無効なHTML文字を渡すとエラーをraiseするようになった

#49120の続きであり、#44948における空文字列""の場合の修正も行う。

動機/背景

#49120 (comment)での議論の結果、tagcontent_tagヘルパーメソッドで無効なHTML文字をバリデーションすることが最終的に決定された。

詳細

tagメソッドとcontent_tagメソッドにHTMLタグ名のバリデーションを追加した。
content_tagメソッドは、指定のタグ名がHTMLの仕様に準拠しているかどうかをチェックする。無効なHTMLタグ名が指定された場合、このメソッドは適切なエラーメッセージと共にArgumentErrorを発生する。

修正前:

content_tag("12p")
#  "<12p></12p>"

content_tag("")
# "<></>"

tag("image file")
#  "<image_file></image_file>"

修正後:

# ArgumentError: Invalid HTML5 tag name: 12p
content_tag("12p") # Starting with a number

# ArgumentError: Invalid HTML5 tag name:
content_tag("") # Empty tag name

# ArgumentError: Invalid HTML5 tag name: "image file"
tag("image file") # Contains a space

cc @byroot
同PRより


つっつきボイス:「今まではtagcontent_tagに空文字みたいな無効なものを渡すと通っちゃっていたのか」「直接そういうものを渡すコードを書くことはあまり考えられないけど、渡すものをeachで回してtagcontent_tagに渡したりすると起きそう」「細かいけど、見逃すとつぶすのが面倒なヤツですね」


なおビューヘルパーのtagメソッドが登場して以来、content_tagは随分前からレガシー記法とされていますが、現在も一応使えます。詳しくは以下の記事をどうぞ。

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

🔗 ActiveStorage::Blob#signed_idexpires_atオプションが追加

動機/背景

有効期限付きのURLをexpires_inオプションで生成すると、生成のたびにURLが変更される。
以下の例に示すように、expires_atオプションを使うことで、ブラウザキャッシュが一定期間効くようになる。

rails_blob_path(user.avatar, disposition: "attachment", expires_at: 2.hours.from_now.beginning_of_hour)
<%= image_tag rails_blob_path(user.avatar.variant(resize: "100x100"), expires_at: 2.hours.from_now.beginning_of_hour) %>

詳細

ActiveStorage::Blob#signed_idとURLヘルパーにexpires_atオプションを追加する。
同PRより


つっつきボイス:「Durationを指定する従来のexpires_inオプションに加えて、絶対時刻を指定するexpires_atオプションも追加されたんですね」「expires_inで有効期限を指定すると期限切れ前でも生成のたびにURLが変わっていたのね」「キャッシュが効かなくなって悲しいヤツだ」「委譲先のActiveRecord::SignedId#signed_idには元々expires_inexpires_atが両方あるので、Active Storageのファイルアップロード機能にもexpires_atを足したんですね」「なるほど、今の挙動は変えずにオプションを追加したのか」「英語的にも、前置詞がatだとその時刻きっかりで、inだとその時刻までのどこかの時刻というニュアンスと合ってますね: "I'll be there in 5 minutes."が"あと5分以内に着くから"となるので、もっと早く着いてもよかったりします」

参考: signed_id -- ActiveRecord::SignedId - Ruby on Rails API

# activestorage/config/routes.rb#L59
  direct :rails_storage_redirect do |model, options|
    expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in }
+   expires_at = options.delete(:expires_at)

    if model.respond_to?(:signed_id)
      route_for(
        :rails_service_blob,
-       model.signed_id(expires_in: expires_in),
+       model.signed_id(expires_in: expires_in, expires_at: expires_at),
        model.filename,
        options
      )
    else
-     signed_blob_id = model.blob.signed_id(expires_in: expires_in)
+     signed_blob_id = model.blob.signed_id(expires_in: expires_in, expires_at: expires_at)
      variation_key  = model.variation.key
      filename       = model.blob.filename

      route_for(
        :rails_blob_representation,
        signed_blob_id,
        variation_key,
        filename,
        options
      )
    end

🔗 Action MailerにFormBuilderを導入

関連: #48477

メールでフォームを使うことは一般的ではないが、可能。また、コントローラーとメーラーの間でビューを共有することも可能。
現在は、グローバル設定と異なるdefault_form_builderをコントローラーで設定しても、同じビューを使っているメーラーで同じFormBuilderがデフォルトにならない。これを修正するため、このプルリクではメーラー用のdefault_form_builderメソッドを追加する。このメソッドの振る舞いはコントローラーにある類似のメソッドと同じ。
同PRより


つっつきボイス:「Action Mailerで送信するメールのフォームをコントローラと同様に上書きしてカスタマイズしたりできるようになったということですか?」「そうみたいです」「メールでフォームを使うのって、たしかに可能だと思いますけど今までやったことなかったな〜」「ユースケースよくわからないけど、Google Formsみたいなフォームをメールに埋め込んでフォーム送信可能にするみたいなのをやりたいのかも?🤔」

参考: ActionMailer::FormBuilder - Ruby on Rails API
参考: Google Forms: オンライン フォーム作成ツール | Google Workspace

🔗 リファクタリング: deep_mergeActiveSupport::DeepMergeableに切り出した

ActiveSupport::DeepMergeableモジュールにより、クラスは単純にmerge!(other, &block)メソッドを実装するだけで、deep_mergedeep_merge!メソッドを提供できるようになる。値は、deep_merge?と互換性がある場合にのみディープマージされる。デフォルトでは、同じクラス(またはそのサブクラス)のインスタンスだけを含む。deep_merge?をクラスでオーバーライドすることで、ディープマージ可能な値のドメインをさらに制限または拡張することも可能。

このプルリクによって、振る舞いがわずかに変更される。
従来のHash#deep_mergeHashインスタンスのみをディープマージしていたが、互換性のある任意のDeepMergeableインスタンスもディープマージするようになる。


これは、#45369のコメントにヒントを得た。現時点のActiveSupport::DeepMergeable:nodoc:になっているが、publicにしたいという要望があれば:nodoc:を削除してもよい。
同PRより


つっつきボイス:「Active Supportのdeep_mergeって何と何をマージするんでしたっけ?」「ハッシュの中にハッシュがあるみたいなヤツでよかったと思います↓」「マージする側とされる側に同じ項目があると後勝ちになるんですね(引数側が優先される)」

# https://github.com/rails/rails/pull/45411/files#diff-716bf4cfb5055399688aa207b69ef1881e526c21940758beb1ad9b04754443bbR36
# 略
@hash_1 = { a: 1, b: 1, c: { d1: 1, d2: 1, d3: { e1: 1,        e3: 1 } } }
@hash_2 = { a: 2,       c: {        d2: 2, d3: {        e2: 2, e3: 2 } } }

@merged = { a: 2, b: 1, c: { d1: 1, d2: 2, d3: { e1: 1, e2: 2, e3: 2 } } }

# 略
test "deep_merge works" do
  assert_equal Wrapper[@merged], Wrapper[@hash_1].deep_merge(Wrapper[@hash_2])
end

「そもそもRails 7.1のActionController::Parametersdeep_mergedeep_merge!が駆け込みで追加されていたんですね(CHANGELOG)」「とりあえずHashActionController::ParametersにはDeepMergeableincludeされている(#45411コメント)」

「#45369ではHashインスタンスしかディープマージできなかったんですね」「DeepMergeableモジュールをincludeしてmerge!を実装すれば、Structやもっと複雑なものもディープマージしたりブロックを渡して処理を追加したりすることも可能になる、なるほど」「APIドキュメントはまだprivateですけどね」

# activesupport/test/deep_mergeable_test.rb
class DeepMergeableTest < ActiveSupport::TestCase
  Wrapper = Struct.new(:underlying) do
    include ActiveSupport::DeepMergeable

    def self.[](value)
      if value.is_a?(Hash)
        self.new(value.transform_values { |value| self[value] })
      else
        value
      end
    end

    delegate :[], to: :underlying

    def merge!(other, &block)
      self.underlying = underlying.merge(other.underlying, &block)
      self
    end
  end

参考: Hash - Ruby on Rails API


「お、今Railsガイドをインクリメンタル検索してました?」「自分はRailsガイドを有料(700円/月)で使っているのでAlgoliaのインクリメンタル検索などの機能が使えます」「これいいな〜❤️」

参考: Proプラン - Railsガイド
参考: Site Search & Discovery powered by AI | Algolia

インタビュー: 超高速リアルタイム検索APIサービス「Algolia」の作者が語る高速化の秘訣(翻訳)

🔗 Range#overlap?に空のrangeを渡した場合の振る舞いを修正

動機/背景

従来の#overlap?は、rangeが事実上「空」の場合にも誤ってtrueを返していた。

(2...2).overlap? 1..2 # => true
(1..2).overlap? 2...2 # => true

詳細

Range#overlap?はRuby 3.3の実装で修正済みなので、このコミットはRuby 3.3より前の場合も修正する。

追加情報

追加したテストはRubyリポジトリから引っ張ってきたものであり、実装もCの修正をRubyバージョンにしたもの。
同PRより


つっつきボイス:「2...2は空だからどのrangeとも重ならない、言われてみればたしかに」「overlap?は前からRailsのActive Supportにあるみたいだけど、Ruby 3.3.0-preview2ではまだ動かないので、これからRuby 3.3に入るoverlap?が先に修正されたということか」「Railsにはエイリアスのoverlaps?もあるのね」

参考: Feature #19839: Need a method to check if two ranges overlap - Ruby master - Ruby Issue Tracking System
参考: overlap? -- Range - Ruby on Rails API

「空の概念はいろいろ難しい」「ゼロとかnilとかNULLみたいなものって間違いの元になりやすくて怖いですよね」

🔗 SQLite3のdatabase.ymlにretriesオプションを追加

動機/背景

SQLite3のbusy_timeoutは、低レベルのbusy_handler関数固有の実装だが、これはtimeout(単位はms)に達するまで「指数関数バックオフ」で単純にリトライする(ただし真の指数関数ではなく、バックオフが[1, 2, 5, 10, 15, 20, 25, 25, 25, 50, 50 ,100]ミリ秒ずつ増加し、以後はステップごとに100ミリ秒ずつ増加する)。
これをdatabase.ymlファイル内のtimeoutオプション経由でアクセス可能にするのは、便利ではあるものの不十分。

SQLite3をproduction向けRailsアプリケーションのデータベースエンジンとして利用することがますます一般的になっている。SQLite3のパフォーマンス最適化では、バックオフを待つ代わりに、ビジーなコネクションを直ちに再試行することが推奨されている。

詳細

このプルリクは、database.ymlに新しいretriesオプションのサポートを追加する。このオプションはシンプルなbusy_handler関数で使われ、指定した最大回数まで指数関数的バックオフなしでビジーなコネクションをリトライするようになる。
同PRより


つっつきボイス:「先週に続いてSQLite3に強い@fractaledmindさんのプルリクで、しかもバックオフ絡みです(ウォッチ20231004)」「今回はジョブのリトライではなくてSQLite3のデータベースコネクションが切れたときのリトライなので、最初のうちはバックオフしないで積極的にリトライをかけるように修正したのね」「PostgreSQLやMySQLのリトライはどんな戦略なのかちょっと気になりますね」

# activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L714
        def configure_connection
-           @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout])) if @config[:timeout]
+         if @config[:timeout] && @config[:retries]
+           raise ArgumentError, "Cannot specify both timeout and retries arguments"
+         elsif @config[:timeout]
+            @raw_connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout]))
+         elsif @config[:retries]
+           retries = self.class.type_cast_config_to_integer(@config[:retries])
+           raw_connection.busy_handler do |count|
+             count <= retries
+           end
+         end

          raw_execute("PRAGMA foreign_keys = ON", "SCHEMA")
        end

🔗 Active Jobのscheduled_atの値をTimeとしてシリアライズ/デシリアライズするようになった

動機/背景

このプルリクを作成した理由は、ジョブのscheduled_atの値をシリアライズ/デシリアライズすることでジョブ実行中にアクセス可能にするため。

このプルリクでは、scheduled_atenqueued_atの振る舞いを調整して、内部で(エポック秒のFloatや文字列ではなく)Timeオブジェクトを使うようにする。この変更により、主要な set(wait:, wait_until:)インターフェースを利用するユーザーにとってscheduled_atが透過的になり、値を直接設定する場合でも壊れなくなる(例: job.scheduled_at = 10.minutes.from_now.to_f)。アダプタインターフェースは変更していない(引き続きエポック秒のFloatパラメーターを引数として受け取る)。

詳細

この変更は当初2018年に@adamnotoによって#32071で提案されたが、scheduled_atがバックエンドのキューアダプタの実装詳細とみなされたためクローズされた。

あれからエコシステムに多少の変化が生じたのと、scheduled_atがActive Jobでどう処理されているかを再検討する必要があると思ったので、このプルリクを再度オープンした。

エコシステムの変化: 私は自分自身のgood_job gemバックエンドと、Active Job向けに設計されたskiplockについて触れておきたい。これらは5年前に以前のプルリクがクローズされた時点では存在していなかった。これらのActive Jobネイティブアダプタを使うことで、スケジュールされたジョブやリトライの再スケジュールでscheduled_atが直接使われるようになる。今日のActive Jobは価値を主張する立場にあり、適切でない場合でも、アダプタは実行前に値をオーバーライド/再割り当て可能になる(通常は job.provider_job_idを使う)。

Active Jobの変化: ActiveJob::Baseインスタンスにインターフェイスがより集中するようになってきている(例: #setの導入(#43434)や perform_all_laterのインターフェイス(#46603)など)。エンキュー前にインスタンスに設定された属性を、実行中のエンキュー後も利用できるようになるとよい。

この変更を再開するきっかけとなったのは、最近見かけたsidekiq-expiring-job gemをActive Jobのbefore_performで簡単に実装できれば良いと思ったこと。

例:

class ApplicationJob < ActiveJob::Base
  ExpiredJobError = Class.new(StandardError)

  discard_on ExpiredJobError

  before_perform do |job|
    raise ExpiredJobError if job.scheduled_at < 30.minutes.ago
  end
end

最後に、scheduled_atはunixタイムスタンプなので、大好きというわけではない。このプルリクがマージされたら、非推奨サイクルを回してenqueued_at#35238)と同様にDateTimeに変換するようにしてもよい(何ならマージされる前でもよい)。

追加情報

関連issue:


つっつきボイス:「以前も紹介したgood_jobというgemの作者によるプルリクです(ウォッチ20200803)」「good_jobはPostgreSQLのスケジューリング機能を使ってActive Jobのジョブキューを管理するんですね」

bensheldon/good_job - GitHub

「実行中のジョブからscheduled_atを取れるようになるとどんな点が嬉しいですか?」「非同期ジョブを管理するのって実は難しくて、人間が目で監視するならSidekiqのコンソールとかで見ればいいんですが、ジョブ同士が関連しているような場合なんかにプログラム的にscheduled_atのような情報を取りたいことはありますね」「なるほど」「たとえばジョブがスケジューリングされた時刻と実際にワーカーを生成して実行開始した時刻が乖離していないかをチェックして、乖離が大きい場合はワーカー数を調整するとかかな: データベースに書いてもジョブで書き込みに失敗したら情報を取れなくなるかもしれないので、ジョブから取れるようになるといいかもしれませんね👍」

🔗 MySQLのスキーマダンプで引用符のエスケープが重複するバグを修正

概要

MySQLは、生成されたCHECK制約式内の引用符を自動的にエスケープする。
Railsがスキーマを(schema.rbに)ダンプすると、生成されたスキーマ内に含まれる引用符が\\'のように二重エスケープされてしまう。

これを修正するため、フェッチした式でMySQLが行うエスケープを削除する。

修正: #42424

その他の情報

テストをcheck_constraint_test.rbに書くと以下のようなコードになるので、MySQL固有のテストとすることにした。

if current_adapter?(:Mysql2Adapter)
  assert_equal "`name` <> 'forbidden_string'", constraint.expression
elsif current_adapter?(:PostgreSQLAdapter)
  assert_equal "(name)::text <> 'forbidden_string'::text", constraint.expression
else
  assert_equal "name != 'forbidden_string'", constraint.expression
end

コードでわかるように、MariaDBすら引用符の扱いがMySQLと異なっている。
同PRより


つっつきボイス:「わかりやすいバグ」「MySQLとMariaDBで振る舞いが違っているというのがちょっとびっくりですね」

参考: MariaDB - Wikipedia
参考: DBごとのSQLのクォーテーションを整理したった - Qiita

🔗Rails

🔗 Rails 7.1がリリース

Rails 7.1.0 がリリースされました

つっつきボイス:「ついさっき(注: 日本時間の10/5夕方)Rails 7.1がとうとうリリースされましたね🎉」「概要はRailsガイドの7.1リリースノート↓にもう少し詳しく載っていますし、TechRachoのRails 7.1リリース記事からリンクしている一連のCHANGELOG記事もリリース版のものに更新しました」

参考: Ruby on Rails 7.1 リリースノート - Railsガイド

「本家リリース記事に載っている目玉機能については以下でもう少し詳しく読めます↓」

「なお7.1では新たにガイドが2つ追加されています↓」

参考: Active Record の複合主キー - Railsガイド
参考: Rails アプリケーションのエラー通知 - Railsガイド


「一連の7.1 CHANGELOG記事は単なる翻訳ではなく、更新ソースやテストやドキュメントやスレッドにも目を通して、可能な限りコードも実際にRailsコンソールで動かしてみました: たぶん人生で一番たくさんCHANGELOGを読んだと思います😂」「CHANGELOGまだ1/3ぐらいしか読めてない😅」「特にActive RecordのCHANGELOGは200件近くあるので書いててきつかったです」「ボリュームの多さがヤバい」「CHANGELOGの原文にはプルリクidが載っていないんですが、読む人が一番欲しい情報だと思ったので、GitHubのblame表示で履歴をたどりながら実際のプルリクやコミットへのリンクや関連記事へのリンクや補足といった便利情報もムキになって追加しました」「お疲れさまです〜」

参考: ファイルの行ごとのリビジョン履歴の表示 -- ファイルの表示 - GitHub Docs

🔗 bun.lockbをGitで管理する


つっつきボイス:「Rails 7.1でサポートされた例のBun(ウォッチ20230926)で使われているロックファイルがバイナリ形式でGitに乗せにくいんだそうで、どうやってGitで差分を見られるようにするかを調べた記事です」「3つほど方法が示されているけど、Bun公式の方法があるといいですよね」「Rails 7.1でもこのあたりをケアする必要がありそうかな?」

参考: Bun — A fast all-in-one JavaScript runtime

oven-sh/bun - GitHub

🔗Ruby

🔗 YARPがprismにリネーム

ruby/prism - GitHub


つっつきボイス:「Ruby 3.3で導入される予定のYARP(パーサー)gemが、つい先頃prismという名前にリネームされたことを以下のRailsプルリクで知りました↓」「お、prismですか」

ruby/prism - GitHub

「#49438でRipperをprismに置き換えるとパフォーマンスが倍近くなってますね↓」

# #49438より
Warming up --------------------------------------
               Prism   173.000  i/100ms
              Ripper    97.000  i/100ms
Calculating -------------------------------------
               Prism      1.731k (± 1.5%) i/s -      8.823k in   5.098289s
              Ripper    942.045  (± 4.1%) i/s -      4.753k in   5.054548s

Comparison:
               Prism:     1731.0 i/s
              Ripper:      942.0 i/s - 1.84x  slower

「個人的にはprismという名前は、光をプリズムで分光するアナロジーがパーサーの振る舞いとよく合うので、パーサーの名前としてとてもいいなと思いました」「JavaScriptの人たちがPrism.jsのことかと思っちゃいそうですけどね」「ありゃ、それもそうか😅」

参考: プリズム - Wikipedia
参考: Prism -- prismjs.com

Rubyパーサーを一新するYARPプロジェクトの全容と将来(翻訳)


今週は以上です。

バックナンバー(2023年度第4四半期)

週刊Railsウォッチ: Rails 7.1.0.rc1と7.1.0.rc2がリリース、SQLite3コンフィグの最適化ほか(20131004)

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

Rails公式ニュース


CONTACT

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