- Ruby / Rails関連
週刊Railsウォッチ: Rails 7.1の複合主キー対応が引き続き進む、exceptメソッドにwithoutエイリアスが追加ほか(20230425前編)
こんにちは、hachi8833です。2週間のご無沙汰でした。
お知らせ: 来週の週刊Railsウォッチは5/2(火)短縮版のみとなります。
🔗Rails: 先週の改修(Rails公式ニュースより)
また本家に引き離されてしまったのでピッチあげます。
- 公式更新情報: Ruby on Rails — March 31st 2023 edition 🌸
- 公式更新情報: Ruby on Rails — A new conference, new Action Mailer callbacks and more!
🔗 複合主キー関連
🔗 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(複合主キー)の略なのか」
🔗 自動生成されるインデックス名を上限で切り詰めるようになった
- PR: Fix Rails generated index name being too long by mscoutermarsh · Pull Request #47753 · rails/rails
自動生成されるインデックス名の上限が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を更新してTableDefinitionにindexメソッドを追加することで、長すぎるインデックス名を切り詰める新しいデフォルト機能をオーバーライド可能にする。
同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を削除するのはもっともですね👍」
参考: 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]
🔗 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を開発している大手の会社ですね、懐かしい」
🔗 細かめの改修
「ここからは細かめの改修をまとめました」
🔗 "Did you mean?"のrakeタスク読み込みの重複を修正
#47208の続き。
UnrecognizedCommandErrorはprinting_commandsを呼び出し、"Did you mean?"を提案する。Printing_commandsはRakeCommand::rake_tasksを呼び出し、Rakeタスクがまだメモ化されていない場合は読み込む。しかしタスクが既にRakeCommand::performで読み込み済みの場合(ただしRakeCommand::rake_tasksでメモ化されていなかった場合)、タスクがもう一度読み込まれてしまう。これは、タスクファイルで定数を定義している場合などに、定数の再定義の警告を引き起こす可能性がある。そのため、このコミットでは
UnrecognizedCommandErrorをraiseする前にRakeCommand::performからタスクをメモ化するようにする。
同PRより
🔗 ActionController::Parametersのexceptメソッドにwithoutエイリアスが追加
動機/背景
ActionController::Parametersは多くの点でHashWithIndifferentAccessとして振る舞う。しかし、背後(delegate先)のパラメータハッシュに存在するexceptメソッドのエイリアスがなく、この振る舞いの違いが紛らわしい。
このプルリクを作成した理由は、これまでなかったエイリアスを追加するため。詳細
このプルリクは、ActionController::Parametersクラスのexceptのエイリアスとしてwithoutを追加する。
同PRより
つっつきボイス:「ActionController::Parametersのexceptにwithoutというエイリアスができたのか」「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::Attachablesにto_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_deliver、after_deliver、around_deliverが追加
動機/背景
このプルリクは、Action Mailerに配信用コールバック(before_deliver、after_deliver、around_deliver)を追加する。メリットとしては、配信のobserverやinterceptor的な振る舞いを、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四半期)
- 20230412前編 複合主キーの実装が進む、Rails公式のバグ再現用テンプレートほか
- 20230406後編 Rubyオブジェクトモデルクイズの最難問ほか
- 20230405前編 Arel::Nodes::NodeにAPIドキュメントが追加、rubocop-mdほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

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