こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 暗号化関連
🔗 EncryptedConfigurationが一部のHashメソッドで誤った値を返していたのを修正
- PR: Fix EncryptedConfiguration not behaving like Hash by skipkayhil · Pull Request #48556 · rails/rails
動機/背景
修正: #48554
EncryptedConfigurationは以前更新された。これにより、キーをメソッドのように呼び出し可能になった。その後さらに更新され、HashとOrderedOptionsの両方の振る舞いをするようになった。しかし2度目の変更で誤ってInheritableOptionsインスタンスが別のInheritableOptionsインスタンス内部にネストしてしまうようになった。これは、外部の
InheritableOptionsにキーが存在しない場合、内部のInheritableOptionsにフォールバックするため、ほとんどの場合期待通りに機能していた。しかし、外部のInheritableOptionsをすべてのキーについて知っているかのように扱おうとするメソッド(例:#keys、#to_h、#to_json)は失敗する。詳細
このコミットは、余分な外側のInheritableOptionsインスタンスを削除することで問題を修正する。
同PRより
つっつきボイス:「このInheritableOptions.newが余計だったということですね↓: シンプルな修正」「7-0-stableは影響受けてなかった」
# activesupport/lib/active_support/encrypted_configuration.rb#L94
def options
- @options ||= ActiveSupport::InheritableOptions.new(deep_transform(config))
+ @options ||= deep_transform(config)
end
参考: Rails API ActiveSupport::EncryptedConfiguration
🔗 Rails.application.config#inspectでsecret_key_baseを出力しないよう修正
Rails.application.config#inspectを呼び出すと、@secret_key_baseを含むすべての属性が表示されてしまう(21c3455以降)。
inspectメソッドをオーバーライドしてクラス名のみを表示することで、機密情報が誤って出力されることを防止できる。変更前:
Rails.application.config.inspect "#<Rails::Application::Configuration:0x00000001132b02a0 @root=... @secret_key_base=\"b3c631c314c0bbca50c1b2843150fe33\" ... >"変更後
Rails.application.config.inspect "#<Rails::Application::Configuration:0x00000001132b02a0>"同PRより
つっつきボイス:「前回の改修のバグ修正だそうです(ウォッチ20230704)」「あ〜、@secret_key_baseがinspectで見えてたのか: これは修正必要ですね」「これまでinspectが定義されてなくて全部表示されていたのを、inspectをオーバーライドしてclass.nameとobject_id以外を表示しないようにしたのか↓」「まあ任意の変数を参照・inspectできる状態という前提の場合は、環境変数を参照すれば見えてしまうと思うので、ログなどで意図しない出力をしてしまう場合の対策なのかなと思います」
# railties/lib/rails/application/configuration.rb#L530
+ def inspect # :nodoc:
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
+ end
🔗 複合主キー関連
🔗 has_one関連付けで複合主キーのdependent: :nullifyをサポート
動機/背景
このプルリクを作成した理由は、現在のhas_one複合主キー関連付けでdependent: :nullifyが動作しないため。詳細
このプルリクは、ActiveRecord::Associations::HasOneAssociationのnullificationコードを変更し、外部キーカラムをイテレーションして、それらがモデルの主キーに属さない場合にnullifyする。追加情報
複合主キーの一部がnullになってはいけないシナリオを作成するため、テストモデルを追加しなければならなかったが問題ないだろうか?
同PRより
つっつきボイス:「今回も複合主キー関連の実装が続いてますね」「has_oneの複合主キーでdependent: :nullifyを使えるようにした、なるほど」「フック周りとかにも改修が必要なところとかあったりしそうですね」
🔗 has_oneの関連付けで複合主キーの一部を変更したときに関連付けが保存されるよう修正
動機/背景
has_oneの関連付けで複合主キーを使う場合は、has_oneのオーナーの複合主キーの一部が変更されたら、変更を所属オブジェクトの外部キーに反映する必要がある。これまでうまく動作していなかった理由は、
#_record_changed?が複合主キー関連付けを処理できていなかったためで、オーナーの主キーが変更されたときに所属オブジェクトの外部キーを更新する必要性を自分たちが認識できていなかった。詳細
#_record_changed?のチェックは変えずに、複合主キーまたは外部キーに対応するように調整する。つまり、オーナーの主キーを構成するすべての属性がレコードに存在することを確認し、次に複合外部キーの値がオーナーの主キーと一致しないことを確認する。これはレコードが変更されたことを表し、それに応じて外部キーの値も更新する必要があることを意味する。
同PRより
つっつきボイス:「これもhas_oneの修正ですね」「_record_changed?で呼び出されるassociation_foreign_key_changedが修正された↓」「複合主キーをサポートし始めると、やることがいろいろ見つかりますね」
# activerecord/lib/active_record/autosave_association.rb#L482
def association_foreign_key_changed?(reflection, record, key)
return false if reflection.through_reflection?
- record._has_attribute?(reflection.foreign_key) && record._read_attribute(reflection.foreign_key) != key
+ foreign_key = Array(reflection.foreign_key)
+ return false unless foreign_key.all? { |key| record._has_attribute?(key) }
+
+ foreign_key.map { |key| record._read_attribute(key) } != Array(key)
end
🔗 複合主キーのモデルでeager loadingできるようになった
動機/背景
このプルリクを作成した理由は、複合主キーを持つモデルと関連付けのeager loadingが現在壊れているため。詳細
このプルリクエストでは、
joinの依存関係の代入を変更することで、複合主キーを持つノードを適切に処理するようにする。追加情報
has_many/has_oneリレーションのためにdistinct_relation_for_primary_keyも変更する必要が生じた。以下のような変換の必要があるため、基本的にArray#transposeを使うことにした。
[[id], [other_id]]を[[id, other_id]]に変換[[cpk_id_1, cpk_id_2], [other_cpk_id_1, other_cpk_id_2]]を[[cpk_id_1, other_cpk_id_1], [cpk_id_2, other_cpk_id_2]]に変換同PRより
つっつきボイス:「複合主キーのモデルでeager loadingするのはなかなか難しそうだけど、複合主キーを扱う可能性のあるところはひととおり対応しないといけなくなるのは仕方がないですね」「たしかに」「それでもRailsの複合主キー関連がかなりいいテンポで修正されているのがすごい: 想像だけどこれまで複合主キーのgemをやってた人たちもRailsの複合主キー改修に参加してたりするのかな?」「ノウハウありそうなので参加してたら心強いですね」
🔗 Active Recordでidをenum値の名前として使えないようにした
- PR: Disallow
idas an enum value in Active Record (v2) by ghiculescu · Pull Request #48536 · rails/rails
#48527の再試行。
上は、
primary_keyの呼び出しによってrailtiesテストが壊れてしまったため、#48534で取り消された。元の問題が引き起こされたのは、ここで
#idが呼び出されたため。そのため、このチェックは値が主キーではなくidの場合にのみ実行する必要があると思う。
同PRより
つっつきボイス:「#idというenum名があるとprimary keyのidなのかenum値なのかが混じっちゃうのはわかる」「idみたいにぶつかりそうな名前をどこまで扱うかって悩ましいですね」
# activerecord/lib/active_record/enum.rb#L320
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
raise_conflict_error(enum_name, method_name, type: "class")
elsif klass_method && method_defined_within?(method_name, Relation)
raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
+ elsif klass_method && method_name.to_sym == :id
+ raise_conflict_error(enum_name, method_name)
elsif !klass_method && dangerous_attribute_method?(method_name)
raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
raise_conflict_error(enum_name, method_name, source: "another enum")
end
end
🔗 Action Textで可能な場合はHTML5サニタイザを使うようになった
- PR: Update Action Text to use HTML5 when available by flavorjones · Pull Request #48522 · rails/rails
動機/背景
Action Textを更新して、利用可能な場合にはHTML5サニタイザを使用し、マークアップの解析にはNokogiri::HTML5を使うようにした。従来は、これらの操作でNokogiriのHTML4パーサーが使われていた。詳細
このプルリクは、ActionText::ContentHelper.sanitizerのデフォルト値をRails::Html::Sanitizer.safe_list_sanitizer.newからRails::Html::Sanitizer.best_supported_vendor.safe_list_sanitizer.newに更新する。.best_supported_vendor
メソッドを利用できるのはrails-html-sanitizer v1.6.0以降であり、これは500ccaaでAction Viewの最小要件となっている。このプルリクエストでは、"最もサポートされている" HTMLドキュメントとドキュメントフラグメントのクラスを返す2つの新しいメソッド(
ActionText.html_document_classおよびActionText.html_document_fragment_class)も追加される。これらのメソッドは、以前コードが直接Nokogiri::HTMLを参照していた箇所で内部的に使われる。Nokogiriでの問題を回避するため、
#cloneから#dupへの変更が必要。HTML5フラグメントでは#cloneが正しく定義されておらず、フラグメントに親ドキュメントが存在しない可能性がある。#dupなら期待通りに動作するので問題はないはず。追加情報
これはRailsでHTML5を使うように更新する取組の一環。
同PRより
つっつきボイス:「この間のHTML5(正式にはHTML Living Standard)対応の続きですね(ウォッチ20230621)」「NokogiriのHTML5パーサーを使うようになった、なるほど」
🔗 to_fsでbeginless/endless rangeをサポート
> (0..1).to_fs(:db) => "BETWEEN '0' AND '1'" > (..1).to_fs(:db) RangeError: cannot get the first element of beginless range from .../activesupport/lib/active_support/core_ext/range/conversions.rb:33:in `first' > (0..).to_fs(:db) RangeError: cannot get the last element of endless range from .../activesupport/lib/active_support/core_ext/range/conversions.rb:33:in `last'期待される動作
> (0..1).to_fs(:db) => "BETWEEN '0' AND '1'" > (..1).to_fs(:db) => "LESS THAN '1'" > (0..).to_fs(:db) => "GREATER THAN '0'"48485より
つっつきボイス:「to_fsは少し前にto_s(:format)が非推奨になって代わりに推奨されたメソッドでしたね(ウォッチ20221025)」「それに..1や0..みたいなrangeを渡せるようになったんですね」
参考: §10.4.2 to_fs -- Active Support コア拡張機能 - Railsガイド
「へ〜、to_fsに:dbを渡すと、idに応答する、つまりid属性を持っていればActive Recordを意識した形で扱えるようになるんですね↓: [user].to_fs(:db)はこのuserがidを持っているので"8456"という文字列を返し、invoice.lines.to_fs(:db)のようにlinesコレクションがそれぞれidを持っているので"23,567,556,12"というidリストを文字列で返す」「なるほど」
to_fsメソッドは、デフォルトではto_sと同様に振る舞います。ただし、配列の中に
idに応答する項目がある場合は、:dbというシンボルを引数として渡すことで対応できる点が異なります。この手法は、Active Recordオブジェクトのコレクションに対してよく使われます。返される文字列は以下のとおりです。[].to_fs(:db) # => "null" [user].to_fs(:db) # => "8456" invoice.lines.to_fs(:db) # => "23,567,556,12"上の例の整数は、
idへの呼び出しによって取り出されたものとみなされます。
§10.4.2to_fs-- Active Support コア拡張機能 - Railsガイドより
🔗 has_one関連付けがautosaveされるときは_read_attributeを使うよう修正
動機/背景
このプルリクを作成した理由は、複合主キーモデルが関連する主キーを:idに設定した場合に正しく自動保存されないため。複合主キーを持つモデルでは、#idアクセサは配列を返すが、実際にはidカラムの値が必要。詳細
このプルリクは、has_oneの自動保存をpublic_sendではなく_read_attributeで行うよう変更し、#idアクセサが使われないようにする。追加情報
既存のアプリの振る舞いが壊れないかどうかについて懸念はあるだろうか?
同PRより
つっつきボイス:「これも複合主キーとhas_oneとidがらみの修正: 配列を返す#idメソッドが複合主キーのモデルで呼ばれないように条件を変更したんですね」「これも地道な作業」
# activerecord/lib/active_record/autosave_association.rb#L446
def save_has_one_association(reflection)
association = association_instance_get(reflection.name)
record = association && association.load_target
if record && !record.destroyed?
autosave = reflection.options[:autosave]
if autosave && record.marked_for_destruction?
record.destroy
elsif autosave != false
- key = reflection.options[:primary_key] ? public_send(reflection.options[:primary_key]) : id
+ key = reflection.options[:primary_key] ? _read_attribute(reflection.options[:primary_key].to_s) : id
if (autosave && record.changed_for_autosave?) || _record_changed?(reflection, record, key)
unless reflection.through_reflection
Array(key).zip(Array(reflection.foreign_key)).each do |primary_key, foreign_key_column|
record[foreign_key_column] = primary_key
end
association.set_inverse_instance(record)
end
saved = record.save(validate: !autosave)
raise ActiveRecord::Rollback if !saved && autosave
saved
end
end
end
end
🔗 関連付け(belongs_to/has_one/has_many)のサブクエリにスコープを適用するようになった(その後取り消し)
例:
has_many :welcome_posts, -> { where(title: "welcome") }
変更前:Author.where(welcome_posts: Post.all) #=> SELECT (...) WHERE "authors"."id" IN (SELECT "posts"."author_id" FROM "posts")変更後:
Author.where(welcome_posts: Post.all) #=> SELECT (...) WHERE "authors"."id" IN (SELECT "posts"."author_id" FROM "posts" WHERE "posts"."title" = 'welcome')Lázaro Nixon
同Changelogより
つっつきボイス:「has_manyにlambda(->)を書くことってあんまりしないかも: 自分なら別メソッドを書くかな」「ちょっとわかりにくいけど、変更後を右にスクロールするとWHERE "posts"."title" = 'welcomeが追加されてますね」「変更前は追加されてないということは振る舞いが変わっているので、これを使っている場合は一応気をつけておく必要はありそう」
「rafaelfrancaのコメントにもbreaking changeとありますね↓: 自分もそう思う」「rafaelfrancaはオプトインをすすめているけど今のところ入ってないみたい」
これは大きなbreaking changeだ。従来バージョンのRailsでは適用されていなかった条件がこの変更で適用されると結果が異なることになる。
@lazaronixon 関連付けに何らかのオプションを渡したらこの振る舞いをオプトインするプルリクを開いてもらってもよいだろうか?例:
has_many :welcome_posts, -> { where(title: "welcome") } , class_name: "Post", apply_on_subqueries: true
#48487のrafaelfrancaのコメントより
なお、#48487は「もっと議論が必要」という理由で取り消されていました。
参考: Revert "Merge pull request #48487 from lazaronixon/scope-subqueries" · rails/rails@e366af5
前編は以上です。
バックナンバー(2023年度第3四半期)
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。


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