こんにちは、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
id
as 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ウォッチタグ)