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

週刊Railsウォッチ: Rails 7.1の複合主キー対応が引き続き進む、exceptメソッドにwithoutエイリアスが追加ほか(20230425前編)

こんにちは、hachi8833です。2週間のご無沙汰でした。

週刊Railsウォッチについて

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

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

お知らせ: 来週の週刊Railsウォッチは5/2(火)短縮版のみとなります。

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

また本家に引き離されてしまったのでピッチあげます。

🔗 複合主キー関連

🔗 dupで複合主キーをリセットするようになった

このコミットは、ActiveRecord::Baseのサブクラスのインスタンスで#dupが呼び出されたときに複合主キーがリセットされることを保証する。例:

class TravelRoute < ActiveRecord::Base
  self.primary_key = [:origin, :destination]
end


route = TravelRoute.new(origin: "NYC", destination: "LAX")
route.dup # => #<TravelRoute origin: nil, destination: nil>

実装の詳細

よくあることだが、選択肢は2つある。
1) @primary_key値を配列にラップしてロジックを統一し、配列の各要素に対してリセットを実行する。
2) composite_primary_key?に基づいて分岐し、不要なアロケーションを回避する。

一般には、アロケーションを回避するために後者の分岐を選択する。しかし長期的にはprimary_keyという内部概念を導入して、配列となる値を常にクラス自体にメモ化するのがよいように思える。こうすることで配列のアロケーションを気にしなくてよくなるし、コードの設計を自分好みのpk_as_an_array.eachのアプローチで統一できる。

テスト
TravelRouteという新しいテストモデルを:origin, :destinationという複合主キーで定義した(テストで直接主キー値を設定したりアサーションしたりする場合、既存のモデルに比べてテストが読みやすいと思えたため)。
同PRより


つっつきボイス:「複合主キーを使っているモデルのインスタンスをdupしたときに属性をリセットするようになった、なるほど」「Active Recordでdupしたことはあまりないんですが、もともと@attributes.reset(@primary_key)をやっているのでdupしても`idは重複しないようになってるんですね↓」

# activerecord/lib/active_record/core.rb#L508
    def initialize_dup(other) # :nodoc:
      @attributes = @attributes.deep_dup
-     @attributes.reset(@primary_key)
+     if self.class.composite_primary_key?
+       @primary_key.each { |key| @attributes.reset(key) }
+     else
+       @attributes.reset(@primary_key)
+     end

      _run_initialize_callbacks

      @new_record               = true
      @previously_new_record    = false
      @destroyed                = false
      @_start_transaction_state = nil
      super
    end

参考: Rails API dup -- ActiveRecord::Core

後でRails 7アプリでdupしてみました。

$ dip rails c --sandbox
$ pattern = Pattern.last.dup
# 略
$ pattern.id  #=> nil
$ pattern.save; pattern.id #=> 8182

🔗 複合主キーでない場合は通常の主キーのみをデフォルトとするようにした

動機/背景
複合主キーサポートの取り組みの一環。

歴史的には、述語ビルダーで扱うリレーションにSELECT済みの値がない場合、モデルの主キーをSELECTする形にフォールバックする。これは、カラムが1つの主キーの場合にはうまくいく傾向がある。

複合主キーの場合は、1個の属性と属性リストを比較すると、ビルドされるSQLがおかしくなる可能性がある。例:

order = cpk_orders(:cpk_groceries_order_1)
subquery = Cpk::Order.where(Cpk::Order.primary_key => [order.id])

# => raises ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: cpk_orders.["shop_id", "id"]
Cpk::Order.where(id: subquery).to_a 

このチェックは、デフォルトが(複合でない)主キーの場合に複合主キーに対してraiseすることで、そうした挙動を防ぐ。当面は、ユーザーが自分でクエリをビルドできる。

詳細
このプルリクはさほど振る舞いを変更しない(この変更があってもなくてもRailsは最終的にraiseする)。その代わり、エラーをSQL構文レベルから予防的な(つまりより有益な)レベルに変更する。このブランチは、複合主キーをデフォルトにする機能が望まれるのであれば、最終的にその機能をサポートすることは可能。しかし欲しいクエリをユーザーがビルドする方法はいくらでもあるので、当面はこれで十分。
同PRより


つっつきボイス:「複合主キーにまだ対応していないものに複合主キーを渡したらraiseするようになったんですね」「cpkはcomposite primary key(複合主キー)の略なのか」

🔗 自動生成されるインデックス名を上限で切り詰めるようになった

自動生成されるインデックス名の上限が62バイトになり、MySQL、Postgres、SQLiteのデフォルトのインデックス名の長さの制限に収まるようになった。
この上限を超えた場合は、以下のように新しいフォーマットで短縮されるようになる。
改修前(長すぎ)

index_testings_on_foo_and_bar_and_first_name_and_last_name_and_administrator

改修後(短縮形)

ix_on_foo_bar_first_name_last_name_administrator_5939248142

インデックス名がデータベース全体で一意になるよう、短縮形にハッシュが追加される。
Mike Coutermarsh
同Changelogより


つっつきボイス:「インデックス名の長さが上限を超えたら、indexをixに置き換えたり末尾をハッシュで埋めたりすることで上限以内にフォールバックするように改修したんですね」「これも複合主キー関連ですね: Railsが自動生成するインデックス名が長くなりすぎてデータベース側で怒られることがあるんですが、特に複合主キーだと上限を超えやすい」「あ〜そういうことですか」「インデックス名を自分で考えずに済めばそれに越したことはないんですけどね」「これはみんなが喜ぶ改修👍」

🔗 Rails 7.1より前のマイグレーションでcreate_tableするときに従来のインデックス名が使われるようにする

動機/背景
このプルリクを作成した理由は、Rails 7.0以前のマイグレーションでcreate_tableブロックの中にインデックスを追加したときに、インデックス名がRails 7.1 以前の形式にならず、#47753で導入された新しいインデックス名形式でインデックスが作成されてしまうため。

このプルリクがないと、ActiveRecord::Migration[7.0]以前のインスタンスに対してcreate_tableブロックのコンテキストでインデックスを追加するマイグレーションを実行すると、デフォルトではインデックス名前が従来の形式にならず、新しい切り詰め形式になってしまう。

詳細
このプルリクは ActiveRecord::Migration::Compatibility::V7_0を更新してTableDefinitionindexメソッドを追加することで、長すぎるインデックス名を切り詰める新しいデフォルト機能をオーバーライド可能にする。
同PRより


つっつきボイス:「マイグレーションでActiveRecord::Migration[7.0]が指定されたらRails 7.0のときの方法でインデックス名を生成するようになった: 互換性維持のための改修ですが、これはまさに、上の#47753でインデックス名の自動生成方法がRails 7.1で変わったからでしょうね」「あ、たしかに」「Rails 7.1へのアップグレードではこのあたりも一応気をつけておこう」

🔗 mini_mimeを削除してmarcelに完全移行


つっつきボイス:「marcelは以前mimemagicのGPL問題で話題になったMIMEタイプを扱うライブラリですね(ウォッチ20210329)」「 marcelとmini_mimeは役割が重複しているのでmini_mimeを削除するのはもっともですね👍」

rails/marcel - GitHub
discourse/mini_mime - GitHub

参考: MIME タイプ (IANA メディアタイプ) - HTTP | MDN

🔗 CHECK制約を含むスキーマのダンプがMySQL 8.0.16以降で正常に動作するよう修正

動機/背景
このプルリクを作った理由は、RailsがMySQL 8.0の新バージョンでCHECK制約を正しく扱わなくなったため。
修正: #47849

詳細
MySQL 8.0.16以降を利用していて、データベースにCHECK制約を持つテーブルがある場合、スキーマをダンプするときに制約の冒頭と末尾の文字が取り除かれる。このため、MySQL 8.0 データベースで:rubyスキーマ形式によるCHECK制約を利用できなくなる。つまり一度ダンプされると再インポートできない。

原因は、MySQL 8.0.16までは、すべてのCHECK制約が余分な丸かっこで囲まれていたため。この振る舞いは、AbstractMySqlAdapter内のエンジンチェックによって正しく処理されるMariaDBの動作とは異なる。
この変更によりMariaDBは引き続き正しく処理されるようになるが、MySQL 8.0.16以降についても同様に動作するよう、データベースのバージョンチェックが追加される。
エクスポートされたCHECK制約が無効で、予期しないホワイトスペースを含む可能性もある。これについても\\\nのシーケンスを含むホワイトスペースを取り除くことで処理可能にする。

追加情報
Dockerで動作するローカル版MySQL 8.0に対して可能な限り徹底的にテストを行ったが、新しい振る舞いのテストを書く方法がわからなかった。スキーマダンパーのCHECK制約を扱うテストは1つしかなく、MySQL固有の振る舞いが含まれていない。
同PRより


つっつきボイス:「CHECK制約付きのスキーマダンプ形式がMySQL 8.0.16で変わったのか〜」「こういう変更はたまに起きるんですよね」

参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.1.20.6 CHECK 制約

MySQL 8.0.16 より前の CREATE TABLE では、次の限定バージョンのテーブル CHECK 制約構文のみが許可されていました。この構文は解析され、無視されます:

CHECK (expr)

MySQL 8.0.16 の時点で、CREATE TABLE は、すべてのストレージエンジンに対して、テーブルおよびカラムの CHECK 制約のコア機能を許可します。 CREATE TABLE では、テーブル制約とカラム制約の両方に対して、次の CHECK 制約構文を使用できます:

[CONSTRAINT [symbol]] CHECK (expr) [[NOT] ENFORCED]

MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.1.20.6 CHECK 制約より

🔗 Backburnerジョブ用のprovider_job_idが実装された

Backburnerジョブ用のprovider_job_idを設定する。
Cameron Matheson
同Changelogより


つっつきボイス:「Elastic Beanstalk向けのActiveJob QueueAdapterでBackburnerのジョブを扱えるようにしたらしい: Backburnerって初めて聞いたけど名前がちょっとかっこいい」「BackburnerはAutodeskのネットワークレンダリングシステムだそうです↓」「Backburnerはハイエンド3DGCで有名なMayaでも使えるらしい: それがActive Jobで扱えるようになるということか」「知らない世界だった」

参考: AWS Elastic Beanstalk(ウェブアプリの実行と管理)| AWS
参考: Maya - Wikipedia
参考: Backburnerとは|3DCGデザイナー専攻|デジタルハリウッドの専門スクール(学校)

Backburnerとは、Autodesk社が提供するネットワークレンダリングシステムで、
Autodesk製品である3ds MaxやMaya、Flame、Smokeといった編集ソフトや、
Pencil+、V-Rayなど特定のソフトでの使用が可能です。
Backburnerとは|3DCGデザイナー専攻|デジタルハリウッドの専門スクール(学校)より

「ところでAutodeskといえばずっと前から建築方面で使われているAutoCADを開発している大手の会社ですね、懐かしい」

参考: AutoCAD - Wikipedia

🔗 細かめの改修

「ここからは細かめの改修をまとめました」

🔗 "Did you mean?"のrakeタスク読み込みの重複を修正

#47208の続き。

UnrecognizedCommandErrorprinting_commandsを呼び出し、"Did you mean?"を提案する。Printing_commandsRakeCommand::rake_tasksを呼び出し、Rakeタスクがまだメモ化されていない場合は読み込む。しかしタスクが既にRakeCommand::performで読み込み済みの場合(ただし RakeCommand::rake_tasksでメモ化されていなかった場合)、タスクがもう一度読み込まれてしまう。これは、タスクファイルで定数を定義している場合などに、定数の再定義の警告を引き起こす可能性がある。

そのため、このコミットではUnrecognizedCommandErrorをraiseする前にRakeCommand::perform からタスクをメモ化するようにする。
同PRより

🔗 ActionController::Parametersexceptメソッドにwithoutエイリアスが追加

動機/背景

ActionController::Parametersは多くの点でHashWithIndifferentAccessとして振る舞う。しかし、背後(delegate先)のパラメータハッシュに存在するexceptメソッドのエイリアスがなく、この振る舞いの違いが紛らわしい。
このプルリクを作成した理由は、これまでなかったエイリアスを追加するため。

詳細
このプルリクは、ActionController::Parametersクラスのexceptのエイリアスとしてwithoutを追加する。
同PRより


つっつきボイス:「ActionController::Parametersexceptwithoutというエイリアスができたのか」「withoutはたしかにわかりやすいかも」「へ〜!」「ここではHashWithIndifferentAccess#withoutと仕様を合わせるためにエイリアスを追加したんですね」

# actionpack/lib/action_controller/metal/strong_parameters.rb#L737
+   alias_method :without, :except

参考: Rails API without -- ActiveSupport::HashWithIndifferentAccess

🔗 Action Textで添付ファイルのデフォルトのテンプレートが見つからない場合の振る舞いをオーバーライド可能になった

動機/背景
Action Textの添付ファイルをレンダリングするとき、背後のattachableが削除されている場合、現在RailsはすべてのAttachables型に対してaction_text/attachables/missing_attachableパーシャルをレンダリングする。消費するアプリケーションはこのパーシャルをオーバーライド可能だが、すべてのAttachablesモデルでグローバルに適用される。

このプルリクは、attachableモデルがこのパーシャルをオーバーライドしてモデル固有のレンダリングを提供できるようにする。

消費するアプリケーションで、レンダリングするマークアップをモデルに応じて変更したい場合がある(例: @でメンションされた場合はMissing user、ファイルアップロードの場合はフォールバック画像を表示する)。

詳細

  • ActionText::Attachablesto_missing_attachable_partial_pathクラスメソッドを追加した
  • 添付ファイルのsgidを解析してモデルを判断できる場合、MissingAttachableをモデルの to_missing_attachable_partial_path に委譲するように変更した
  • 添付ファイルのsgidからattachableモデルを判断できない場合、デフォルトのパーシャルにフォールバックする
    同PRより;cite

つっつきボイス:「この添付ファイルはAction Textのものを指しているんですね」「attachableが見つからない場合の挙動をモデルごとにオーバーライドしてカスタマイズ可能にしたんですね👍」「お〜なるほど」

🔗 Active Supportの数値の単位にゼタバイトが追加


つっつきボイス:「ついにゼタバイト!」「ギガの次がテラ、ペタ、エクサで、その次がゼタですか」「1ゼタバイトは1兆ギガバイト😆」「いつか使うときが来るのかな」

# activesupport/lib/active_support/core_ext/numeric/bytes.rb#L3
class Numeric
  KILOBYTE = 1024
  MEGABYTE = KILOBYTE * 1024
  GIGABYTE = MEGABYTE * 1024
  TERABYTE = GIGABYTE * 1024
  PETABYTE = TERABYTE * 1024
  EXABYTE  = PETABYTE * 1024
+ ZETTABYTE = EXABYTE * 1024

参考: ゼタ - Wikipedia

🔗 バックグラウンドジョブのenqueue呼び出し元をログ出力できるようになった

バックグラウンドジョブのenqueue呼び出し元をログに表示してデバッグしやすくするためのverbose_enqueue_logs設定オプションを追加した。

ログ行の例:

Enqueued AvatarThumbnailsJob (Job ID: ab528951-41fb-4c48-9129-3171791c27d6) to Sidekiq(default) with arguments: 1092412064
↳ app/models/user.rb:421:in `generate_avatar_thumbnails'

development環境では、新規およびアップグレードしたアプリケーションでのみ利用可能。依存しているRubyのKernel#callerがかなり遅いので、production環境での利用は推奨されない。
fatkodima
同Changelogより

参考: Kernel.#caller (Ruby 3.2 リファレンスマニュアル)


つっつきボイス:「お〜、verbose_enqueue_logsコンフィグをオンにすると、ジョブをエンキューしたときに呼び出し元もログに出してくれるようになった: これはありがたい👍」「development環境ならデフォルトでオンでもいいぐらい」
「ジョブキューのバグって、たいていキューを入れるとき・取り出すとき・処理するときのどれかで起きますよね」「そうそう」

🔗 Active Storageのvariantを個別に削除可能にした

背景:
Active Storageのvariantを作成する際、ActiveStorage::VariantRecordが挿入され、その後でファイルがアップロードされる。アップロードは失敗することもあるので、ActiveStorage::VariantRecordが存在するにもかかわらずファイルが見つからない場合がある。

ファイルが見つからない場合は、対応するActiveStorage::VariantRecordを削除する必要があるが、(attachable.variant(resize_to_limit: [100, 100]).destroy)のように)variantを1個だけ削除するAPIは存在しない。
同PRより


つっつきボイス:「Active Storageのvariantを個別に削除できるようになったそうです」「variantsの種類を増やしたり減らしたりしたときに、全部のvariantを再生成・削除せずに特定のvariantだけをクリーンアップしたい、といった用途は普通にあり得るので、これもありがたい👍」

参考: Rails API ActiveStorage::Variant
参考: §9 画像を変形する -- Active Storage の概要 - Railsガイド

🔗 Action Mailerにbefore_deliverafter_deliveraround_deliverが追加

動機/背景
このプルリクは、Action Mailerに配信用コールバック(before_deliverafter_deliveraround_deliver)を追加する。メリットとしては、配信のobserverやinterceptor的な振る舞いを、MailオブジェクトだけではなくActionMailer::Baseのインスタンスのコンテキスト内でも使えるようになること。

ユースケース:

  • Active Recordモデルのdelivered_atの値が正確に更新されるようになる(特にdeliver_laterの場合)。
  • 配信プロバイダのmessage_idで何かする(例: ユーザーやメッセージへの参照と共に保存して、開封/クリック/迷惑メールに対するプロバイダのWebフックと連携可能にする)。
  • 通常と異なる配信エラーを処理する。無効なメールアドレスなどはメーラーが配信を試行するまで出力されないがメーラーの完全なコンテキストを持っているので、Userレコードにメールが配信されなかったという情報を追加するなど。

追加情報

#42139もこれと似ているが、メーラーのコンテキストを提供することまではやっていない。

注意: Action Mailerのrescue_fromは、メーラーのアクション処理とレンダリングステップ、そして配信ステップをすべてラップしている(実質2回)。自分は配信コールバックでdeliverだけをラップしたかった(既存の他の*_actionコールバックがアクション処理とレンダリングステップをラップしているので)。以下はその擬似コード。

processed_mailer = rescue_from do
  around_action do
    render_mail
  end
end

rescue_from do
  around_deliver do
    deliver(processed_mailer)
  end 
end

同PRより


つっつきボイス:「Action Mailerにdeliver系コールバックが3つ追加された: Action Mailerはコントローラの一種とみなせるので、こういうコールバックはあっていいと思います👍」

参考: Action Mailer の基礎 - Railsガイド


前編は以上です。

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

週刊Railsウォッチ: ShopifyのRubyパーサーyarp、RJITを書いた理由ほか(20230413後編)

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

Rails公式ニュース


CONTACT

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