- 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ウォッチタグ)