- Ruby / Rails関連
週刊Railsウォッチ: Active Recordのenumにエラーをraiseしないvalidateオプションが追加ほか(20230913前編)
こんにちは、hachi8833です。昨日以下のツイートに気づいて7.1.0のマイルストーンを見に行ってみると、issueが一気にcloseされていました。
Rails 7.1.0.beta1 の足音が近づいている。https://t.co/qHqcPA9XUj
— Koichi ITO (@koic) September 12, 2023
参考: 7.1.0 Milestone
その後v7.1.0.beta1
タグが追加され、本日Rails 7.1 Beta 1がリリースされました🎉。
参考: Ruby on Rails — Rails 7.1 Beta 1: Dockerfiles, BYO Authentication, More Async Queries, and more!
お知らせ: 来週の週刊Railsウォッチはお休みとし、通常記事を公開します。
🔗Rails: 先週の改修(Rails公式ニュースより)
先週の改修が公式更新情報に追いついてしまったので、久しぶりにコミットリストから見繕いました。
ドキュメントの更新が少し増えています。7.1リリースノートもだいぶ更新されてきたようです。
🔗 merge
にrewhere
オプションを渡すことが非推奨化された
6.1リリースで、より一貫した
merge
の振る舞いを得るために、#39250でmerge
にrewhere
オプションが追加された。7.0では、
merge
の呼び出しで両方の条件をデフォルトで維持する振る舞いが削除された(515aa1e
)リリースノートとオリジナルの非推奨警告の両方で、6.1アプリを7.0アプリに移行する方法として
rewhere: true
が表示されている。7.0は既にリリースされているので、
merge
のオプションとしてのrewhere
を非推奨化して、rewhere
しないバージョンのmerge
を削除可能になっている。同PRより
つっつきボイス:「merge
メソッドは7.0で既にrewhere: true
オプションを指定したのと同じ振る舞いに変わっているので、7.1からはrewhere
オプションを付けることが非推奨になって7.2でrewhere
オプションが削除されるということみたいですね」「rewhere: true
もrewhere: false
も今後merge
に渡せなくなるのか」
# activerecord/lib/active_record/relation/spawn_methods.rb#L
def merge!(other, *rest) # :nodoc:
options = rest.extract_options!
+
+ if options.key?(:rewhere)
+ if options[:rewhere]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Specifying `Relation#merge(rewhere: true)` is deprecated, as that has now been
+ the default since Rails 7.0. Setting the rewhere option will error in Rails 7.2
+ MSG
+ else
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `Relation#merge(rewhere: false)` is deprecated without replacement,
+ and will be removed in Rails 7.2
+ MSG
+ end
+ end
+
if other.is_a?(Hash)
Relation::HashMerger.new(self, other, options[:rewhere]).merge
elsif other.is_a?(Relation)
Relation::Merger.new(self, other, options[:rewhere]).merge
elsif other.respond_to?(:to_proc)
instance_exec(&other)
else
raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
end
end
参考: Rails API merge
-- ActiveRecord::SpawnMethods
「そういえばRails 6.1の頃に同じカラムをmerge
したときの振る舞いを変更しようという話がありましたね(ウォッチ20200615)」
参考: § 8.3 主な変更点 -- Ruby on Rails 7.0 リリースノート - Railsガイド
- 同じカラム上で条件をマージした場合に両方の条件が維持されなくなり、常に後者の条件によって置き換わるようになった。
# Rails 6.1 (IN句はマージする側の等値条件によって置き換えられる) Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob] # Rails 6.1 (競合する条件がどちらも存在する: 非推奨) Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => [] # Rails 6.1 でrewhereを用いてRails 7.0の挙動に移行する) Author.where(id: david.id..mary.id).merge(Author.where(id: bob), rewhere: true) # => [bob] # Rails 7.0 (IN句の振る舞いは同じで、マージされる側の条件が常に置き換えられる) Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob] Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => [bob]
今回の変更の目的は、マージの振る舞いがこれまで一貫していなかったのを統一すること。
現在は、マージされる側(mergee)の条件がマージする側(merger)によって置き換えられるのは、双方のarelノードが等価条件またはIN句の場合のみだった。
言い換えると、マージされる側の条件が等価条件でもIN句でもない場合(between
やgt
やlt
など)、双方の条件は同じカラムであっても維持されていた。
これではマージの振る舞いに熟知していないと予測が難しい。
元々自分は、この振る舞いは意図したものではなく単なる実装上の問題だと推測していた。理由は、merge
より後に導入されたunscope
やrewhere
の挙動はmerge
よりも一貫性が高くなっているため。
等価条件やIN句は条件のほとんどを占めるのが普通なので、この問題を踏んだことのある人はほとんどいないだろうと推測しているが、一貫性に欠ける現在の振る舞いを非推奨化し、将来のUXを改善するために今後完全に統一したいと思う。
ウォッチ20200615の#39328プルリクメッセージを再録
🔗 define_attribute_methods
でエイリアス属性メソッドも再定義できるようになった
修正: #48931
undefine_attribute_methods
は、属性メソッドだけでなくエイリアス属性メソッドも削除するようになった。このコミットでは、define_attribute_methods
を変更して、エイリアス属性が宣言されている場合にメソッドを再定義するようにした。これにより、undefine_attribute_methods
の実行後にエイリアスメソッドを元に戻すオプションがアプリケーションやライブラリで提供されるようになる。実装の詳細
既存の
alias_attribute
内部リストに加えて、aliases_by_attribute_name
も追加できるようにする。これは属性名ごとのエイリアス属性リストを保持する。このリストは、メソッドが再定義されるたびにエイリアスを復元するために、define_attribute_methods
で使われるようになる。
基本的には、属性でないものがエイリアス化されている場合を除き、define_attribute_methods
がほとんどの場合においてエイリアス属性メソッドと実際の属性メソッドの両方を定義する責任を持つようになる。同PRより
つっつきボイス:「属性メソッドを削除するundefine_attribute_methods
が既にエイリアスメソッドも削除するようになったから、属性メソッドを定義するdefine_attribute_methods
もエイリアスメソッドを定義するようにした: このあたりはあまり使ったことない機能だけど、動作が一貫する方がよいでしょうね👍」
Railsは、#48533でエイリアス属性メソッドが定義されるターゲットを変更した。これにより、
undefine_attribute_methods
は属性メソッドだけでなくエイリアス属性メソッドもクリーンアップするようになった。この振る舞いの変更は意図したものだが、適切に文書化やテストが行われていなかった。このコミットでは、Active Modelの変更ログで新しい動作を明文化し、テストでもその振る舞いをカバーした。#49015より
🔗 gzipされたスキーマキャッシュがGit履歴のロールバックで変更とみなされないよう修正
- PR: ensure identical md5 sums for gzip schema cache by aleksclark · Pull Request #49166 · rails/rails
動機/背景
このプルリクは、同一バージョンのスキーマキャッシュ.gzが同じmd5チェックサムを持つことを保証し、gitで誤った変更を通知しないようにする。
詳細
gzipのデフォルト設定では、変更時刻がファイルヘッダに含まれるため、以下を実行すると
db/schema_cache.yml.gz
が変更される。rails db:schema:cache:dump rails db:rollback rails db:migrate rails db:schema:cache:dump
開発中はマイグレーションを前後にロールバックする必要があり、.gzファイルは中身がわからないため、混乱を引き起す。
これを緩和するために、キャッシュの書き込み前に
zipper.mtime = 0
を設定する。同PRより
つっつきボイス:「スキーマの内容は変わっていないのにスキーマファイルのメタデータに記録される変更日付が変わっていた場合に変更扱いされていたのを修正、なるほど」「開発中にこれが起きると困るヤツだ」「スキーマのファイルオープンでzipper.mtime = 0
を追加したんですね↓」
# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L468
def open(filename)
FileUtils.mkdir_p(File.dirname(filename))
File.atomic_write(filename) do |file|
if File.extname(filename) == ".gz"
zipper = Zlib::GzipWriter.new file
+ zipper.mtime = 0
yield zipper
zipper.flush
zipper.close
else
yield file
end
end
参考: Zlib::GzipWriter#mtime=
(Ruby 3.2 リファレンスマニュアル)
🔗 Active Recordのenumにエラーをraiseしないvalidate
オプションが追加された
- `enumにバリデーションオプションが追加された。
class Contract < ApplicationRecord enum :status, %w[in_progress completed], validate: true end Contract.new(status: "unknown").valid? # => false Contract.new(status: nil).valid? # => false Contract.new(status: "completed").valid? # => true
class Contract < ApplicationRecord enum :status, %w[in_progress completed], validate: { allow_nil: true } end Contract.new(status: "unknown").valid? # => false Contract.new(status: nil).valid? # => true Contract.new(status: "completed").valid? # => true
Edem Topuzov
同Changelogより
動機/背景
#13971はかなり前からあるissue。
@dhhは2017年に貢献することを提案したが、オープンされたプルリク#41730 は2021年以降メンテナンスされていなかったので、このプルリクのコードレビューのコメントを考慮するようにした。詳細
このプルは、enum型に
:validate
オプションを追加する。保存する前にenum値をバリデーションしたい場合は、以下のように
:validate
オプションを使う。class Conversation < ApplicationRecord enum :status, %i[active archived], validate: true end conversation = Conversation.new conversation.status = :unknown conversation.valid? # => false conversation.status = nil conversation.valid? # => false conversation.status = :active conversation.valid? # => true
以下のように追加のバリデーションオプション(
nil
を許す)も渡せる。class Conversation < ApplicationRecord enum :status, %i[active archived], validate: { allow_nil: true } end conversation = Conversation.new conversation.status = :unknown conversation.valid? # => false conversation.status = nil conversation.valid? # => true conversation.status = :active conversation.valid? # => true
それ以外の場合(
:validate
オプションがない)は、ArgumentError
がraiseされる(現在の標準的な振る舞い)。class Conversation < ApplicationRecord enum :status, %i[active archived] end conversation = Conversation.new conversation.status = :unknown # 'unknown'は無効なステータス(ArgumentError)
追加情報
この変更は破壊的にならないように意図している。
同PRより
つっつきボイス:「お、enum定義にvalidate: true
を追加することでエラーをraiseせずにenumをバリデーションできるようになったんですね👍: 今まではenumの値リストにないものを代入した時点でArgumentError
になってた」
参考: § 17 enum
-- Active Record クエリインターフェイス - Railsガイド
🔗 複合主キーにidを含むモデルでprimary_key: :id
を推測するようになった
動機/背景
従来の複合主キーモデルでは、関連付けに対して
primary_key
またはquery_constraints
オプションを指定する必要があった。これらのオプションがない場合、CompositePrimaryKeyMismatchError
が発生する。ほとんどの場合、複合主キーには
:id
列が含まれることが期待される。関連付けでは既に:id
が関連付けの主キーとして使われている(そして関連するモデルは<cpk_model>_id
を外部キーとして推測する)ことが期待されている。アプリケーション全体で複合主キー関連を使うためにユーザーが
primary_key: :id
の定義を要求される必要はなく、むしろ:id
カラムが使われるべきと推測できる。ユーザーは引き続き、関連付けでprimary_key:
またはquery_constraints:
を指定することで、この動作をオーバーライドできる。モデルの複合主キーに
:id
が含まれていない場合、Railsはそれらの関連付けで主キーを推測しない。ユーザーは引き続きquery_constraints
またはprimary_key
を指定する必要がある。この変更の前は、複合主キーモデルの関連付けを設定するために以下の手順が必要だった。
class Order self.primary_key = [:shop_id, :id] has_many :order_agreements, primary_key: :id end class OrderAgreement belongs_to :order, primary_key: :id end
この変更後は、
primary_key
オプションを指定する必要はなくなる。class Order self.primary_key = [:shop_id, :id] has_many :order_agreements end class OrderAgreement belongs_to :order end
詳細
このプルリクのほとんどの変更は、以下の場合に関連付けの
primary_key
を推測することに関係している。
- a) 関連付けの所有者が複合主キーモデルであり、かつ
- b) 複合主キーに
:id
が含まれている場合最終的に
#save_has_one_association
をリファクタリングしたことで、この複合キー推論ロジックを#compute_primary_key
と共有できるようになった。また、
CompositePrimaryKeyMismatchError
に関連するロジックをテストしていたモデルの一部も変更する必要があった。これは、(Active Recordはこれらの関連付けでprimary_key
を適切に推測できるようになったことで)これらの関連付けがもはや不正な形式ではなくなったため。
自分は「壊れている」モデルの主キーを変更して、id
列を除外することにした。これにより、Railsが関連付け上で主キーを推測しないようになり、CompositePrimaryKeyMismatchError
が期待通りに発生する。同PRより
つっつきボイス:「これも複合主キーの改修ですね」「これまでの複合主キーモデルで複合主キーにidが含まれている場合はprimary_key: :id
をhas_many
側にもbelongs_to
側にも書かないといけなかったけど、複合主キーにidが含まれている場合はprimary_key: :id
を書かなくてもよくなった: こう書けるのはたしかに嬉しい👍」「複合主キーにidが含まれていればprimary_key: :id
であることが多いでしょうという前提なんですね」「どこまでその前提でいけるかはなかなか難しい問題かもしれませんが」
なお、つっつき後に新しい複合主キーガイドがmainブランチに追加されていることに気づきました↓。
参考: rails/guides/source/active_record_composite_primary_keys.md at main · rails/rails
🔗 where
でトリプルドット...
rangeを使うとunscope
が効かない問題を修正
- PR: Fix unscope not working when where by tripe dot range by ippachi · Pull Request #48095 · rails/rails
unscope
が特定のケースで動作しなかったのを修正。# 改修前 Post.where(id: 1...3).unscope(where: :id).to_sql # "SELECT `posts`.* FROM `posts` WHERE `posts`.`id` >= 1 AND `posts`.`id` < 3"
# 改修後 Post.where(id: 1...3).unscope(where: :id).to_sql # "SELECT `posts`.* FROM `posts`"
修正: #48094
Kazuya Hatanaka同Changelogより
動機/背景
修正: #48094
where
でトリプルドット...
rangeを使うとunscope
が効かない。詳細
fetch_attribute
をactiverecord/lib/arel/nodes/and.rb
で定義した(unscope
は述語を除外するかどうかの判断にfetch_attribute
を使っている)。同PRより
つっつきボイス:「ダブルドット..
は終端の値を含むrangeで、トリプルドット...
は終端を含まないrangeでしたね」「Arelにfetch_attribute
を新たに定義することで修正している↓」
# activerecord/lib/arel/nodes/and.rb#L21
+ def fetch_attribute(&block)
+ children.any? && children.all? { |child| child.fetch_attribute(&block) }
+ end
参考: class Range
(Ruby 3.2 リファレンスマニュアル)
なお、この修正はRails 7.0.8にもバックポートされました↓(ce75465)。
🔗 ActionController::Parameters
にextract_value
メソッドが追加
パラメータからシリアライズ済みの値を抽出する
ActionController::Parameters#extract_value
メソッドを追加。params = ActionController::Parameters.new(id: "1_123", tags: "ruby,rails") params.extract_value(:id) # => ["1", "123"] params.extract_value(:tags, delimiter: ",") # => ["ruby", "rails"]
Nikita Vasilevsky
同Changelogより
このコミットは、複合主キーを持つモデルでシリアライズ済みの複合
id
値を抽出する主要な手段として、ActionController::Parameters
にextract_value
メソッドを追加する。#49020で
to_param
メソッドを拡張して複合識別子をサポートした後、Action Viewヘルパー(form_for(cpk_record)
など)はデフォルトでアンダースコア("_"
)で結合された複合キーのリソースURLをビルドするようになった。これにより、結合された値をコントローラーが元の複合主キー値に戻す必要が生じる。extract_value
はそのためのヘルパーメソッド。def show identifier = params.extract_value(:id, delimiter: TravelRoute.param_delimiter) @travel_route = TravelRoute.find(identifier) ... end
潜在的な懸念
- 実装があまりにも単純なので、新しいメソッドを追加するほどの正当な理由がない
extract_value
という名前は既存のextract!
メソッドに似ているが、APIが異なる同PRより
つっつきボイス:「Shopifyによる改修です」「ActionController::Parameters
インスタンスに渡されたid: "1_123"
のような複合主キーをextract_value
メソッドで["1", "123"]
のように配列の形で取り出せるようになった: 複合主キーは前回も話したように(ウォッチ20230906)デリミタ付きでシリアライズされるので、コントローラでそれを元に戻せるextract_value
メソッドはあって欲しい👍」「まさに前回の#49020の続きなんですね」「実装はsplit
メソッド一発という非常にシンプルなもの↓」
# actionpack/lib/action_controller/metal/strong_parameters.rb#934
def extract_value(key, delimiter: "_")
@parameters[key].split(delimiter)
end
参考: § 10.7.3 split(value = nil)
-- Active Support コア拡張機能 - Railsガイド
前編は以上です。
バックナンバー(2023年度第3四半期)
週刊Railsウォッチ: IRB 1.8.0でデバッグ機能強化、Ruby Prize 2023開催決定ほか(20230908後編)
- 20230906前編 システムテストでPlaywrightをサポート、to_paramのデリミタを変更可能にほか
- 20230829前編 Active Storageのミラーアップロードが非同期に、Rackアプリを手作りほか
- 20230824後編 週刊Railsウォッチ: ArelでCAST関数サポート、webdrivers依存を解消、YJIT高速化ほか
- 20230823前編 Rails 7.0.7に含まれているRails 7.0.6のバグ修正ほか
- 20230809 Rails 7.0.5のcreate_association挙動変更取り消し、YJITの性能を最大限引き出す方法ほか
- 20230803後編 Railsフラグメントキャッシュ経由の情報漏洩に注意ほか
- 20230802前編 Active Storageバリアントの事前変換、Linkヘッダープリロードのオプトアウトほか
- 20230727後編 Rubyにdefp導入の提案、IRB 1.7.3リリースほか
- 20230725前編 config.autoload_libとconfig.autoload_lib_onceが追加ほか
- 20230721後編 Kaigi on Rails 2023プロポーザル募集、rubocop-magic_numbersほか
- 20230719前編 複合主キー関連の実装進む、Action TextでHTML5サニタイザほか
- 20230705後編 AWS LambdaでRailsをRackで動かすLambyほか
- 20230704前編 productionのforce_ssl=trueがデフォルトで有効に、rakeタスクをthorで書くほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)