- Ruby / Rails関連
週刊Railsウォッチ: Ruby 3.3.0-preview1リリース、in_order_ofのバグ修正ほか(20230525後編)
こんにちは、hachi8833です。RubyKaigi 2023でKeebKaigi周りをまったくチェックできなかったのが悔やまれます。
ところで "Initial-V, Final Stage!"はまつもとさんにも観ておいてもらいたいですw 🚗💨 #keebkaigihttps://t.co/0UTnNO4dxH https://t.co/ohigO5GJMM
— Kakutani Shintaro (@kakutani) May 24, 2023
🔗Rails: 先週の改修(Rails公式ニュースより)
昨日に引き続き改修を見ていきます。
🔗 バグ修正
🔗 in_order_of
でネストがすべてフラットになる問題を修正
動機/背景
edge Railsで自分のアプリのどこが壊れるか試してみたら、ここが最初に壊れた。
in_order_of
を使っている箇所にハッシュの配列があるのだが、今回の変更でハッシュの配列がソートされなくなり、値がフラット化された長い配列になってしまった。
実際のオブジェクト:{:closed=>{:value=>0, :status=>"Closed"}, :opened=>{:value=>1, :status=>"Opened"}, :temporarily_closed=>{:value=>2, :status=>"Temporarily closed"}, :recommended=>{:value=>3, :status=>"Recommended"}, :declined=>{:value=>4, :status=>"Declined"}, :submitted=>{:value=>5, :status=>"Submitted"}, :invited=>{:value=>6, :status=>"Invited"}}
従来:
irb(main):001:0> Cafe.symbolized_status_map.in_order_of(:first, [:opened, :submitted, :recommended, :invited, :temporarily_closed, :closed, :decline d]) [[:opened, {:value=>1, :status=>"Opened"}], [:submitted, {:value=>5, :status=>"Submitted"}], [:recommended, {:value=>3, :status=>"Recommended"}], [:invited, {:value=>6, :status=>"Invited"}], [:temporarily_closed, {:value=>2, :status=>"Temporarily closed"}], [:closed, {:value=>0, :status=>"Closed"}], [:declined, {:value=>4, :status=>"Declined"}]]
現状:
irb(main):001:0> Cafe.symbolized_status_map.in_order_of(:first, [:opened, :submitted, :recommended, :invited, :temporarily_closed, :closed, :decline d]) => [:opened, {:value=>1, :status=>"Opened"}, :submitted, {:value=>5, :status=>"Submitted"}, :recommended, {:value=>3, :status=>"Recommended"}, :invited, {:value=>6, :status=>"Invited"}, :temporarily_closed, {:value=>2, :status=>"Temporarily closed"}, :closed, {:value=>0, :status=>"Closed"}, :declined, {:value=>4, :status=>"Declined"}]
そして、以下のようなことをやっている部分で壊れた。
info
は当然nil
になった😅<% Cafe.symbolized_status_map.in_order_of(:first, [:opened, :submitted, :recommended, :invited, :temporarily_closed, :closed, :declined]).each do |sym, info| %>
このプルリクはこの問題を修正して元に戻す。
詳細
このプルリクは#47805の実装を改善する。
追加情報
これは意図した振る舞いなのかどうかをDiscordで聞いたところプルリクを投げるようにアドバイスをもらったので、ここにいます😄
同PRより
つっつきボイス:「in_order_of
はEnumerable
に入ったもの(ウォッチ20210330)とActiveRecord::QueryMethods
に入ったもの(ウォッチ20210831)があるけど、これはActiveRecord::QueryMethods
の方ですね」「ハッシュの配列が誤って全部フラットになったら、そこから先のコードは当然動かなくなりますね」
参考: Rails API in_order_of
-- ActiveRecord::QueryMethods
参考: Rails API in_order_of
-- Enumerable
「そしてこれも修正は1箇所だけ↓」「わかりやすくて嬉しい」flatten(1)
にすることで最初の階層だけをフラットにするのか」「ちなみにcompact
はnil
を配列やハッシュから要素ごと消せるメソッドですね」「compact
も便利そう、今度使ってみます」
# activesupport/lib/active_support/core_ext/enumerable.rb#L196
def in_order_of(key, series)
- group_by(&key).values_at(*series).flatten.compact
+ group_by(&key).values_at(*series).flatten(1).compact
end
参考: Array#flatten
(Ruby 3.2 リファレンスマニュアル)
参考: Array#compact
(Ruby 3.2 リファレンスマニュアル)
参考: Hash#compact
(Ruby 3.2 リファレンスマニュアル)
「ところで今ググったらこんな記事を見つけました↓」「[1,2,3,4,5].compact!
みたいにnil
を含まない配列だとnil
が単品で返される...だと?」「これはビックリ」「compact
に!
が付いているけど破壊的ではなくて元の配列はそのままなんですね」「他にもこんな挙動をするメソッドがあった気がする🤔」
参考: Rubyのcompact! に注意。 - Qiita
🔗 pg gem 1.5.0で不要な非推奨警告が表示されるのを修正
最近リリースされたpg gem 1.5.0で以下の警告が出るようになった(Ruby 3.2.2とRails 7.0.4.3)。
PG::Coder.new(hash) is deprecated. Please use keyword arguments instead! Called from ../ruby/3.2.2/lib/ruby/gems/3.2.0/gems/activerecord-7.0.4.3/lib/active_record/connection_adapters/postgresql_adapter.rb:980:in `new'
#48046より
つっつきボイス:「あ、この警告は自分のRails 7.0.4.3アプリでも最近minitestで出るようになってました↓」
$ dip minitest
# 略
PG::Coder.new(hash) is deprecated. Please use keyword arguments instead! Called from /usr/local/bundle/gems/activerecord-7.0.4.3/lib/active_record/connection_adapters/postgresql_adapter.rb:980:in `new'
PG::Coder.new(hash) is deprecated. Please use keyword arguments instead! Called from /usr/local/bundle/gems/activerecord-7.0.4.3/lib/active_record/connection_adapters/postgresql_adapter.rb:980:in `new'
Started with run options --seed 45774
37/37: [==============================================================================] 100% Time: 00:00:02, Time: 00:00:02
Finished in 2.52181s
37 tests, 65 assertions, 0 failures, 0 errors, 0 skips
「これも修正はシンプルで、**
でハッシュを渡すようにしてますね↓」「Ruby 2.7〜3.0で変更された書き方ですね」「まだ残ってたのか」「7-0-stableブランチにもバックポートされているので、次のマイナーリリースで警告は出なくなるそうです」「ありがたい」
# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L1085
def update_typemap_for_default_timezone
if @raw_connection && @mapped_default_timezone != default_timezone && @stamp_decoder
decoder_class = default_timezone == :utc ?
PG::TextDecoder::TimestampUtc :
PG::TextDecoder::TimestampWithoutTimeZone
- @stamp_decoder = decoder_class.new(@stamp_decoder.to_h)
+ @stamp_decoder = decoder_class.new(**@stamp_decoder.to_h)
@raw_connection.type_map_for_results.add_coder(@stamp_decoder)
@mapped_default_timezone = default_timezone
# if default timezone has changed, we need to reconfigure the connection
# (specifically, the session time zone)
reconfigure_connection_timezone
true
end
end
🔗 Rack::Test::UploadedFile.new
でStringIO
が例外を発生する問題を修正
動機/背景
このプルリクを作成した理由は、私たちが使っているRack::Test::UplaodedFile
がおかしいのではと思ったため。このオブジェクトは実際のファイルオブジェクトでインスタンス化可能(この部分についてはテストスイートで広範囲にわたってテストされている)だが、StringIO
でもインスタンス化できる。残念ながら後者には#open
メソッドがないため例外が発生する。分岐を増やし、背後にどんなオブジェクトがあるかに応じて
Rack::Test::UploadedFile
を使うようにする必要がある(このプルリクではダックタイピングで分岐する方法を提案しているが、他の案も歓迎する)。詳細
このプルリクでは、ActiveStorage::Attached::Changes::CreateOne#upload
メソッド(およびprivateの#find_or_build_blob
メソッド)を変更して、#open
メソッドに応答しない場合はRack::Test::UploadedFile
で#open
を呼ばないようにする。ここではオブジェクト自身を利用している(ファイルを開くのと同様にオブジェクトから読み取り可能)。
追加情報
rack-test
gemのメンテナーによると、このgemの振る舞いは期待通りであるとのこと(rack/rack-test#336)。
同PRより
つっつきボイス:「StringIO
は文字列にIO
のメソッドを持たせるクラスなのか」
参考: class StringIO
(Ruby 3.2 リファレンスマニュアル)
参考: class IO
(Ruby 3.2 リファレンスマニュアル)
「修正はcase
文の場合分けを増やす形で対応してますね↓」
# activestorage/lib/active_storage/attached/changes/create_one.rb#L
def upload
case attachable
- when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
+ when ActionDispatch::Http::UploadedFile
blob.upload_without_unfurling(attachable.open)
+ when Rack::Test::UploadedFile
+ blob.upload_without_unfurling(
+ attachable.respond_to?(:open) ? attachable.open : attachable
+ )
when Hash
blob.upload_without_unfurling(attachable.fetch(:io))
end
end
🔗 before_update
でupdated_at
が更新されない問題を修正
動機/背景
修正されるissue: #45389上のリンク先に書かれているように、
update
のコールバックではupdated_at
タイムスタンプも更新されることが期待される(この振る舞いはsave
コールバックで既に発生するため)。詳細
このプルリクは、update
のすべてのコールバックが実行されたらタイムスタンプが更新され、その中で改変が行われていれば更新として認識され、その結果updated_at
タイムスタンプが確実に更新されるようにする。当初は、タイムスタンプを設定するタイミングをすべてのコールバックが完了した後にしようと考えていたが、タイムスタンプが常にコールバック内でアクセス可能でなければならないため、このコミットで行われた変更では、リグレッションが発生することがあった。
また、コールバック内にタイムスタンプが存在することが予想される関連するいくつかのテストのカバレッジも向上した。これらのテストでは、
create
のコールバックだけでなく、create
、update
、save
のコールバック(それぞれ1種類)をテストするようにした。理由は自分のソリューションに取り組んでいるときに存在した唯一のテストに失敗したためで、リグレッションが発生しないように、これら3種類のコールバックすべてをカバーすることが重要だと考えた。追加情報
デバッグと、失敗するテストの作成(このプルリクで自分がパクったもの)に協力してくれた@markhallenに感謝したい。
同PRより
つっつきボイス:「before_update
でupdated_at
カラムが更新されないことがある問題ですか」super
周りを修正していますね↓」「コールバックがらみだけに、なかなかややこしい修正」
# activerecord/lib/active_record/callbacks.rb#L462
def _update_record
- _run_update_callbacks { super }
+ _run_update_callbacks { record_update_timestamps { super } }
end
# activerecord/lib/active_record/timestamp.rb#L117
def _update_record
+ record_update_timestamps
+
+ super
+ end
+
+ def create_or_update(touch: true, **)
+ @_touch_record = touch
+ super
+ end
+
+ def record_update_timestamps
if @_touch_record && should_record_timestamps?
current_time = current_time_from_proper_timezone
timestamp_attributes_for_update_in_model.each do |column|
next if will_save_change_to_attribute?(column)
_write_attribute(column, current_time)
end
end
- super
- end
-
- def create_or_update(touch: true, **)
- @_touch_record = touch
- super
+ yield if block_given?
end
「ところでcreated_at
やupdated_at
ってメインのロジックでは普通使わないというか、あまり当てにしてはいけないカラムという感じですよね?」「そうそう、ビジネスロジックで必要なタイムスタンプはそれ専用のカラムを作る方がいいでしょうね: created_at
やupdated_at
はRailsが自動的にサポートしてくれますけど、コールバックやフックが絡んでいてアプリケーション開発者が明示的に操作しているものではありませんし、運用上必要になることももちろんありますが、基本的に参考情報と思っておく方がいいんじゃないかな」「なるほど」
🔗 メソッド名になっているコンフィグキーに代入したらエラーを発生するようにした
最近は、メソッド名になっているコンフィグキーにも代入できるようになっている。
require "rails" config = Rails::Railtie::Configuration.new config.eager_load_namespaces = 1
代入した値は当然取り出せない。
config.eager_load_namespaces # [I18n, ActiveSupport, ActionDispatch]
このような代入は無意味なので、APIで防止すべき。現実に以下のような例を目撃したことがある。
config.load_defaults = 7.0
この代入はエラーを出さないので、ユーザーはフレームワークのデフォルトが有効になっていると信じ込んでしまう可能性がある。しかし実際には有効になっていない。
このパッチによって、こうした場合にエラーを発生するようになった。
ここでは
NoMethodError
例外を使うことにした(存在しないセッターメソッドを呼び出そうとしたとも言えるため)。しかしこの特殊な状況をユーザーに理解してもらうために、メッセージはカスタマイズしてある。
同PRより
つっつきボイス:「コンフィグの中にメソッド形式のものがあるって知りませんでした」「config.load_defaults 7.0
なら効くけどconfig.load_defaults = 7.0
みたいに=
を付けてもセッターじゃないから効かないのか!」「=
を付けてもエラーにならなかったら見落としても無理ないですね」「これは大事な修正👍」
参考: Rails アプリケーションを設定する - Railsガイド
「修正箇所を見てて気づいたんですが、コンフィグの処理ってmethod_missing
でやっているんですね」「Railsはコンフィグを増やすたびにメソッドを増やしたりしていないと思うので、method_missing
でやっているのはわかる」
# railties/lib/rails/railtie/configuration.rb#L94
private
+ def actual_method?(key)
+ !@@options.key?(key) && respond_to?(key)
+ end
+
def method_missing(name, *args, &blk)
if name.end_with?("=")
- @@options[:"#{name[0..-2]}"] = args.first
+ key = name[0..-2].to_sym
+ if actual_method?(key)
+ raise NoMethodError.new("Cannot assign to `#{key}`, it is a configuration method")
+ end
+ @@options[key] = args.first
elsif @@options.key?(name)
@@options[name]
else
super
end
end
参考: BasicObject#method_missing
(Ruby 3.2 リファレンスマニュアル)
🔗 ドキュメント・SEO関連
- PR: Add section on INCLUDE option to postgres doc by steve-abrams · Pull Request #48027 · rails/rails
- PR: Add a section on unique constraints to the AR PostgreSQL guide by alpaca-tc · Pull Request #48026 · rails/rails
つっつきボイス:「RailsガイドのPostgreSQLガイドにINCLUDEオプションとUNIQUE制約の記述が追加されたそうです」「お〜いいですね😋」「ちなみにPostgreSQLガイド英語版はだいぶ前からWIP(作業中)のままですが、これでWIPが取れる日が1歩近づいたかな」
参考: Active Record と PostgreSQL - Railsガイド
参考: Active Record and PostgreSQL — Ruby on Rails Guides
- PRリスト: Pull requests · rails/rails
古株の@p8は、検索エンジンでRailsドキュメントの運勢を改善する作業をやってくれています。
公式更新情報より
「2つ目は、検索エンジンがRails APIドキュメントの情報をちゃんと拾ってくれるように修正してくれたそうです↓」「小さな修正がたくさん入ってますね」「地道な改善ありがたい🙏」
# actionpack/lib/action_controller/api.rb#L7
module ActionController
- # = Action \Controller \Base
+ # = Action Controller \API
#
🔗Ruby
🔗 Ruby 3.3.0-preview1がリリース(Ruby公式ニュースより)
つっつきボイス:「RubyKaigi 2023の間にいつの間にか3.3.0-preview1がリリースされていました🎉」「早!」「まだリリースノートの項目に空欄が目立つけど、今後続々と埋まっていくんでしょうね」
「MJITに代わってRJITが入った」「Bison依存がなくなってLramaが入った↓」「Bisonなくなるのか〜」「最近のRubyはパーサーをリニューアルしていて、Lramaもその一環という感じですね」「Ruby 3x3みたいにRubyのパーサーリニューアルにもプロジェクト名あるのかな?」
「YJITもいろいろ高速化されてますね」「--yjit-pause
でYJITを止められるというのもRubyKaigi 2023でも出ていました: ベンチマークでYJITの影響を一時的に止めたりするのに便利そう」
「そうそう、readlineライブラリへの依存もなくなってPure Rubyのrelineに置き換わるそうです」「お〜マジですか!」「BisonやreadlineってRubyをビルドするときにインストールしておくのが定番というイメージですけど、そういう依存が消えるの嬉しい😂」「ちなみにst0012さんもrelineメンテナーの1人です」
🔗 技術書典14が開催
Rubyistの出展情報を求めています🙏 » 技術書典14 - ruby-jp https://t.co/KWoLGxVj3F
— Kakutani Shintaro (@kakutani) May 16, 2023
弊社ソニックガーデンからもRails関連の本が出ます!(って書いたら追加してもらえますか?👀 )
「 技術書典14 」にて3種の技術書を頒布します - SonicGarden 株式会社ソニックガーデン https://t.co/5jbJ7Z5obY https://t.co/UzK21tuiag
— Junichi Ito (伊藤淳一) (@jnchito) May 18, 2023
つっつきボイス:「そういえばもう技術書典の季節か」「Ruby/Rails関係の書籍もいろいろ出展していますね🎉」「オンラインマーケットの書籍数、想像以上に多い」「自分も技術書典に出してみたいネタをこっそり温めてるので来年出したいな〜」
私もつっつき後に『コードレビューで学ぶ Ruby on Rails』をポチりました。
後編は以上です。
バックナンバー(2023年度第2四半期)
週刊Railsウォッチ: withで作成したリレーションをjoinsで指定可能に、キャッシュストアの例外処理を統一ほか(20230524前編)
- 20230502 スライド『Rails 7.1をn倍速くした話』、Rails 7.1でMessagePackをサポートほか
- 20230427後編 第1回Rails Worldが10月に開催、『研鑽Rubyプログラミング』でRuby本体も高速化ほか
- 20230425前編 Rails 7.1の複合主キー対応が引き続き進む、exceptメソッドにwithoutエイリアスが追加ほか
- 20230413後編 ShopifyのRubyパーサーyarp、RJITを書いた理由ほか
- 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ウォッチタグ)