- Ruby / Rails関連
週刊Railsウォッチ: 7.1アップグレードガイドにActive Record暗号化設定の注意事項が追加ほか(20231024前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回も以下のコミット差分からChangelogのあるものを中心に見繕いました。多くは細かなドキュメント修正で、Rails 7.1.1に既に含まれているものもありました。
🔗 Active Modelの内部用にdecorate_attributes
を追加
- PR: Support Active Model attribute type decoration by jonathanhefner · Pull Request #44665 · rails/rails
このプルリクは、属性の型デコレーション(attribute type decoration)をサポートするためにActive Modelに
decorate_attributes
メソッドを追加する。decorate_attributes
はprivateの低レベルAPIであり、ActiveRecord::Base::normalizes
やActiveRecord::Base::enum
などの高レベルAPIによってラップされることを意図している。同PRより
つっつきボイス:「お、このdecorate_attributes
というメソッドがattribute type decorationを行うということですか」「"属性の型デコレーション"としてみましたが、これはいわゆるDecoratorパターンのことでしょうか?」「たぶんそれでよさそうですけど、プルリクメッセージには詳しい説明やChangelogがないので、コードかテストも見てみましょうか」
参考: Decorator パターン - Wikipedia
# activemodel/test/cases/attribute_registration_test.rb#151
test "::decorate_attributes decorates specified attributes" do
attributes = default_attributes_for do
attribute :foo, TYPE_1
attribute :bar, TYPE_2
attribute :qux, TYPE_2
decorate_attributes([:foo, :bar]) { |name, type| MyDecorator.new(name, type) }
end
assert_instance_of MyDecorator, attributes["foo"].type
assert_equal "foo", attributes["foo"].type.name
assert_same TYPE_1, attributes["foo"].type.cast_type
assert_instance_of MyDecorator, attributes["bar"].type
assert_equal "bar", attributes["bar"].type.name
assert_same TYPE_2, attributes["bar"].type.cast_type
assert_same TYPE_2, attributes["qux"].type
end
「属性foo
とbar
とqux
にTYPE_1から3まで型を書いてから、新しいdecorate_attributes
でMyDecorator.new
している」「MyDecorator
というのはこれですね↓」「上のアサーションを見ると、たとえばattributes["foo"].type
がMyDecorator
のインスタンスになっている、なるほど」
# activemodel/test/cases/attribute_registration_test.rb
module ActiveModel
class AttributeRegistrationTest < ActiveModel::TestCase
MyType = Class.new(Type::Value)
Type.register(MyType.name.to_sym, MyType)
TYPE_1 = MyType.new(precision: 1)
TYPE_2 = MyType.new(precision: 2)
MyDecorator = DelegateClass(Type::Value) do
attr_reader :name
alias :cast_type :__getobj__
def initialize(name, cast_type)
super(cast_type)
@name = name
end
end
...
「MyDecorator
は単に型情報を返すだけじゃなくて型変換も行っているっぽい」「Decoratorなので差し替えたりもできるんでしょうね: ActiveRecord::Base::normalizes
や ActiveRecord::Base::enum
でラップすると書かれていますし」
「ところで、decorate_attributes
を公開したら喜ぶ人が割りといそうな気もしますね」「言われてみれば自分も使いたくなりそうな機能、かな?」
🔗 foreign_key:
オプションに誤って配列を渡すと例外を発生するよう変更
関連: #49622
関連付けでは、
foreign_key
オプションを配列として渡すことが許可されたこともサポートされたこともなく、これはRails 7.1でも変わらない。ただし、Rails 7.1では複合主キーがサポートされているため、アプリケーションで誤ってforeign_key:
に配列が渡されることが増えてくる可能性がある。このコミットによって、foreign_key:
が配列として渡されたら例外を発生するようになる。レビュアーへの質問
自分は最終的に
initialize
でチェックを実装した。これにより、関連付けの定義が誤っている場合はアプリケーションを起動できなくなるが、開発者が問題を早期に把握できるようになる。自分は一般的に、起動時のバリデーション実行で例外が発生するかどうかを注意深く見るようにしている。通常なら、アプリケーションで利用できないものや未設定のもの(redis、kafka、elasticsearch、dbなど)があっても起動可能であるべきだと考えている(特に開発中)。しかしこれについては、問題が早急に判明する方が開発者体験の向上につながる良い結果を得られるのではないかと思う。このチェックを
foreign_key
が派生する場所に移動してlazyにチェックすべきと思うのであれば知らせて欲しい。# https://github.com/rails/rails/blob/dcb1d1f4c4987c3352f8c5f94bb44393b0f18252/activerecord/lib/active_record/reflection.rb#L504-L505 elsif options[:foreign_key] options[:foreign_key].to_s
同PRより
つっつきボイス:「関連付けに配列を渡したらinitialize
の時点で容赦なくエラーを出すようになったそうです」「foreign_key
にうっかり配列を渡してしまうというのはありそう」
# activerecord/lib/active_record/reflection.rb#376
def initialize(name, scope, options, active_record)
super()
@name = name
@scope = scope
@options = options
@active_record = active_record
@klass = options[:anonymous_class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
+ validate_reflection!
end
...
+ def validate_reflection!
+ return unless options[:foreign_key].is_a?(Array)
+
+ message = <<~MSG.squish
+ Passing #{options[:foreign_key]} array to :foreign_key option
+ on the #{active_record}##{name} association is not supported.
+ Use the query_constraints: #{options[:foreign_key]} option instead to represent a composite foreign key.
+ MSG
+ raise ArgumentError, message
+ end
「実際、関連付けが間違っていてもアプリの起動はできちゃうんですよ」「え、そうなんですか」「そうそう、実際にその関連付けにアクセスしたときに初めて死にます」「まさに実行時エラーなんですね」
「このあたりをどう扱うかは考えどころかも: たとえば既に使われなくなって消す予定のコードにそういう間違いが残っているだけでもアプリが起動しなくなりますけど、果たしてそれでいいのかという問題もありますね」「あ〜なるほど」
「たしかにアプリは起動できなくなるけど、foreign_key
に配列を渡すというあからさまな間違いなら本番より前に見つかる方がいいでしょうね👍」「プルリクを見た感じでは環境によって振る舞いは変わらないみたいですけど、development環境でチェックすれば十分かもしれませんね」「そうですね、開発中にアプリを動かしたりしていると、使えないはずのhas_many
関連付けがしれっと見つかったりすることもあるので、その種の間違いはむしろdevelopment環境やtest環境でこそエラーになって欲しいかも」
「ちなみに続きのプルリクで、Changelogがmainブランチから消されていました↓」「7-1-stableのChangelogを見ると7.1.1より後に入っているので、以下のメッセージは7.1.x系を指しているということなんでしょうね: 今後7.1.xにアップデートするときは、今まで見落としていた配列渡しでエラーになるかもしれないので、覚えておくといいかも」
🔗 rails new
の--database
オプションなどに誤った値を渡すと通知するようになった
動機/背景
このプルリクを作成した理由は、
rails new
の--database
、--asset-pipeline
、--css
、--javascript
フラグにさまざまなオプションを渡せるにもかかわらず、正しいオプションが指定されているかどうかを確認しているのが--database
オプションしかないため。
--javascript=esbuiild
などのように誤った値が渡されると、意図に反してimportmapが使われたり未定義の振る舞いを示したりする可能性がある。詳細
このプルリクは、他の4つのオプションにも
--database
フラグと同様のチェックを追加し、ユーザーが正しい値を入力できるようにする。追加情報
新しい3つの定数の命名や配置場所に関する意見・提案があれば歓迎。
現状では、この変更によりヘルプテキストのオプションから
(default)
の記述が失われることになるが、現在のデータベースオプションのリストにも(default)
は表示されていない。方法の1つとして、配列の最初の要素をデフォルトとして扱い、それに応じたヘルプテキストを追加して出力する方法が考えられる。同PRより
なお、以下でリファクタリングとChangelogの追加も行われました↓。
つっつきボイス:「rails new
のオプションの話なので継続中の開発には影響しませんが、rails new
をしょっちゅう使う自分には嬉しいです」「渡してよい値のリストをここに切り出したのね↓」
# railties/lib/rails/generators/app_base.rb#21
JAVASCRIPT_OPTIONS = %w( importmap bun webpack esbuild rollup )
CSS_OPTIONS = %w( tailwind bootstrap bulma postcss sass )
ASSET_PIPELINE_OPTIONS = %w( sprockets propshaft )
「その代わり、rails new --help
とかで表示する説明文に(default)
が改修の都合で出なくなったそうです↓」「Railsガイドあたりにデフォルト値を書いておけば十分な気もしますけどね」
# railties/lib/rails/generators/app_base.rb#L74
- desc: "Choose your asset pipeline [options: sprockets (default), propshaft]"
+ desc: "Choose your asset pipeline [options: #{ASSET_PIPELINE_OPTIONS.join(", ")}]"
「ところで、Rubyのメソッドが={}
とかでハッシュのキーワードを受けるようになっていると、存在しないキーワードを渡してもエラーにならないのをちょっと思い出した」「そうそう、HTMLタグを指定できるメソッドなんかだと、どんなタグを渡してもエラーにならないけど、ドキュメントがなくて何を渡すとどう振る舞うのか結局わからないものがあったりしますよね」「Ruby 2.0から入ったキーワード引数なら、存在しないものを渡すとちゃんと怒ってくれますけどね」「違うものを渡すとすぐエラーで知らせてくれると、ちゃんと調べるきっかけになるんですよね」「そういえばJavaScriptのObjectも、存在しないプロパティを呼ぶとエラーにならずに即プロパティが作られるところはRubyのHashにちょっと似ているかも」「まあそういう作法ですから」
🔗 capture
ビューヘルパーがHAMLやSlimで空文字列""
をキャプチャしたときの振る舞いを修正
関連: #47194 (comment)
HAMLもSlimも、キャプチャブロックの戻り値としてバッファを返していたので、
capture
ヘルパーが混乱する。こういうチェックをせずに済めば理想的だが、後方互換性を考えれば妥当なトレードオフ。
同PRより
つっつきボイス:「Changelogを見るとHamlやSlimで空文字列""
がキャプチャされるとバッファ全体を返していたのを修正したということのようですね」
# actionview/lib/action_view/helpers/capture_helper.rb#L47
def capture(*args, &block)
value = nil
@output_buffer ||= ActionView::OutputBuffer.new
buffer = @output_buffer.capture { value = yield(*args) }
- case string = buffer.presence || value
+ string = if @output_buffer.equal?(value)
+ buffer
+ else
+ buffer.presence || value
+ end
+
+ case string
when OutputBuffer
string.to_s
when ActiveSupport::SafeBuffer
string
when String
ERB::Util.html_escape(string)
end
end
参考: HAML - Wikibooks
参考: slim/README.jp.md at main · slim-template/slim
「ところでcapture
って何するメソッドだったかな?」「テンプレートの一部を切り出してテンプレート内で再利用するときに便利そうですね↓」「ビューで使い回したいパーシャルをcapture
で定義できるということですか、なるほど」
参考: § 1.5.1 capture
-- Action View ヘルパー - Railsガイド
<!-- Railsガイドより -->
<% @greeting = capture do %>
<p>ようこそ!現在の日時: <%= Time.now %></p>
<% end %>
<!-- Railsガイドより -->
<html>
<head>
<title>ようこそ!</title>
</head>
<body>
<%= @greeting %>
</body>
</html>
「ちなみに、APIドキュメントだと@greeting
を2箇所で参照していますね↓: Time.now
をERBに直接書くと2つのメッセージで日時の細かい値が微妙にズレてしまうので、日時を含む文字列を@greeting
にキャプチャしてから使えばズレなくなるよというのを見せているんでしょうね」「なるほど」
<!-- APIドキュメントより -->
<% @greeting = capture do %>
Welcome to my shiny new web page! The date and time is
<%= Time.now %>
<% end %>
<!-- APIドキュメントより -->
<html>
<head><title><%= @greeting %></title></head>
<body>
<b><%= @greeting %></b>
</body>
</html>
@greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500"
参考: Rails API: capture
-- ActionView::Helpers::CaptureHelper
🔗 response_body
にEnumeratorを渡すとエラーになるのを修正
- PR: Support handling Enumerator for non-buffered responses by zzak · Pull Request #49616 · rails/rails
現状の欠点は、この種のレスポンスではETagsを生成できなくなることだが、Enumeratorを使う場合はバッファされたレスポンスをキャッシュ可能ということが想定されていない。つまり、Enumeratorではストリーミングレスポンスを生成できないことになる。
修正: #49588
以下も参照: #47092
同PRより
つっつきボイス:「引用されているissue #49588を見ると、まず#47092でActionDispatch::Response
がストリーミングのbodyをRails 7.1でサポートするようになっていて、その後以下のようにresponse_body
にEnumerator.new
を渡すとエラーになっていたのが修正されたという流れのようです」
# #49588再現コードより
def index
self.response_body = Enumerator.new do |enumerator|
10.times do |n|
enumerator << n.to_s
end
end
end
「プルリクメッセージが短かくて状況を把握するのに少々手間取りました😅」「まあエンジニアにしてみればコードとドキュメントを両方書くのは単純に作業が倍になるのと、コードを更新したときにドキュメントの更新を忘れがちになったりすることもあって、プルリクメッセージやドキュメントの量をあまり増やしたくない気持ちになる人がいるのはわかる」「今だったらそれこそAIにドキュメントを書いてもらったらよさそうですよね」「それいいかも」「コードを更新したらAIがAPIドキュメントも自動更新してくれるようになったら最高」「CIに仕込みたい」
後でChatGPT(GPT-4)にこのプルリクのURLを渡してプルリクメッセージを補ってもらいました。
With the introduction of handling enumerators for non-buffered responses, a limitation arises where ETags can no longer be generated for these responses. This change is based on the assumption that when using an enumerator, the intention is not to have a cacheable buffered response. As a consequence of this approach, it's important to note that enumerators cannot be employed to generate streaming responses.
バッファリングされていないレスポンスでEnumeratorのサポートが導入されたことで、そうしたレスポンスでETagを生成できないという制約が生じていた。その変更では、「Enumeratorを渡すことで、レスポンスがキャッシュ可能かつバッファリングされたものではないという意図が示される」という前提をベースとしていた。この方法の結果、Enumeratorではストリーミングレスポンスを生成できなくなっていた点に注意。
🔗 7.1アップグレードガイドにActive Record暗号化設定の注意事項が追加
これは新しいnew_framework_defaultsには記載されているが、古いバージョンからアップグレードするユーザーにとって有用になるよう、アップグレードガイドで明示的に強調するのがふさわしい。
動機/背景
このプルリクを作成した理由は、Rails 7.1へのアップグレードでハッシュアルゴリズムを明示的に設定済みにしておかない限り復号エラーが発生する可能性があるため。
追加情報
#48204でも詳しく議論されているが、結論はこのプルリクで追加したアドバイスに盛り込まれていると理解している。
同PRより
上の#49587の続き。おかしな点があったら指摘して欲しい。
まだ#42922や#48204での議論を組み立てている最中なので、@boomer196や@jorgemanrubiaにもレビューしてもらえるとありがたい。情報が不正確だとproductionでアプリが止まる可能性があるので、可能な限り正確さを担保しておきたい。同PRより
つっつきボイス:「Rails 7.1では44873でActive Record暗号化のハッシュダイジェストアルゴリズムがデフォルトでSHA-1からSHA-256(SHA-2の一種)に変更されてconfig.active_record.encryption.hash_digest_class
でコンフィグ可能になったんですが(ウォッチ20230322)、それに伴ってこの2つのプルリクでいくつか重要な注意事項が追加されました」「ハッシュダイジェストアルゴリズムが変わるということは、暗号を復号できなくなったりするヤツか」
参考: Active Record と暗号化 - Railsガイド
参考: SHA-1 - Wikipedia
参考: SHA-2 - Wikipedia
「Active Record暗号化を使っているRailsアプリを7.1にアップグレードするときは要注意なヤツだ」「忘れた頃にこれを踏んでデータが読めなくなったりする人がいそう」「テストしてちゃんと検証すれば本番前に気づけるとは思いますけどね」
つっつき後にRailsガイドにも反映しました↓。
参考: § 2.11 Active Record 暗号化アルゴリズムの変更について -- Rails アップグレードガイド - Railsガイド
ここで言及されている以下の設定は互いに関連していますが、設定名の後半が同じでも別物なのでご注意ください。
前編は以上です。
バックナンバー(2023年度第4四半期)
週刊Railsウォッチ: Kaigi on Rails 2023関連イベント情報公開、複合主キーのlocality解説記事ほか(20231018後編)
- 20231017前編 Active Storageのしくみを詳しく解説するDiscussion投稿ほか
- 20231011 Rails 7.1.0リリース、YARPがprismにリネームほか
- 20131004 Rails 7.1.0.rc1と7.1.0.rc2がリリース、SQLite3コンフィグの最適化ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)