Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Packwerkの詳しい解説書『Gradual Modularization for Ruby and Rails』ほか(20221101前編)

こんにちは、hachi8833です。Kaigi on Rails 2022の動画がYouTubeにアップロード完了しました🎉

参考: Kaigi on Rails 2022 - YouTube

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

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

お知らせ: 来週は週刊Railsウォッチはお休みとし、通常記事を公開いたします。

🔗Rails: 先週の改修(Rails公式ニュースより)

🔗 同一レコードでafter_commit :destroyの重複トリガーを解消

この変更は、以下のようにデータベース内のレコードがdestroyされた場合にのみafter_commit :destroyをトリガーするようにする。

record = MyModel.find(42)
record.destroy # => after_commitコールバックをトリガーする
record.destroy # => after_commitコールバックを「トリガーしない」

従来は#27248after_commit :destroyの振る舞いが更新され、同じレコードのインスタンスが2つあって一方がdestroyされると、他方のレコードのdestroyafter_commitコールバックでトリガーされなかった。しかし1つ目のレコードの以後のdestroyは引き続きトリガーされるようになっていた。この変更によって現在は以下のように振る舞っている。

record = MyModel.find(42)
same_record = MyModel.find(42)

record.destroy # => after_commitコールバックをトリガーする
record.destroy # => after_commitコールバックを再度トリガーする
same_record.destroy # => after_commitコールバックを「トリガーしない」

同PRより


つっつきボイス:「これは同じレコードでafter_commit :destroyが複数回呼ばれたときにそもそもどう振る舞うべきかという話でもあるでしょうね」「改修前と改修後の振る舞いの違いがコメントにありましたけど、こんなふうに変わるんですね↓」「たしかにdestroyが2回呼ばれるのはよくないけど、そういうときはエラーにしてもいいんじゃないかなという気持ちはあります」

✅: after_commit(on: :destroy)コールバックをトリガーする
❌: after_commit(on: :destroy)コールバックを「トリガーしない」

現在の振る舞い

new_record? persisted? destroyed?
DB行が存在
DB行が不在

私の変更提案

new_record? persisted? destroyed?
DB行が存在
DB行が不在

#46197のコメント(bensheldon)より

「元の振る舞いがアプリケーションのコードで当てにされていたりするでしょうか?」「同じレコードインスタンスを2回destroyすることは普通ないかなとも思ったけど、アプリのつくりによっては削除ボタンを連打したりすると起きる可能性もありそうですね」「なるほど」

🔗 ErrorReporterで複数のエラークラスを扱えるようになった

動機/背景
ErrorReporterは、以下のようなエラーハンドリングコードの定型文を置き換えるのが目的。

begin
  do_something
rescue SomethingIsBroken => error
  MyErrorReportingService.notify(error)
end

上を以下の統一的なインターフェイスに置き換える。

Rails.error.handle(SomethingIsBroken) do
  do_something
end

ただし、以下のように複数のエラークラスを処理できるようにしたいこともある。

Rails.error.handle(ArgumentError, TypeError) do
  [1, 2, 3].first(x) # ただし`x`は`-4`や`'4'` の可能性もある
end

このプルリクは、エラークラスのリストをRails.error.handleRails.error.recordに渡せる機能を追加する。後方互換性も完全に維持される。
同PRより


つっつきボイス:「assert_raisesにエラーを複数渡すとき↓みたいなノリで、ErrorReporterにエラーのクラスを複数渡せるようになったんですね」「ErrorReporterはRails 7で入った機能でしたね(ウォッチ20211129)」

# activesupport/test/error_reporter_test.rb#71
  test "#handle can be scoped to several exception classes" do
    assert_raises ArgumentError do
      @reporter.handle(NameError, NoMethodError) do
        raise ArgumentError
      end
    end
    assert_equal [], @subscriber.events
  end

  test "#handle swallows and reports matching errors" do
    error = ArgumentError.new("Oops")
    @reporter.handle(NameError, ArgumentError) do
      raise error
    end
    assert_equal [[error, true, :warning, "application", {}]], @subscriber.events
  end

参考: Minitest API Method: Minitest::Assertions#assert_raises — Documentation for minitest (5.16.3)
参考: Rails API ActiveSupport::ErrorReporter

🔗 ciphertext_forが暗号化前の値を返す問題を修正

このコミット以前のciphertext_forは、永続化されていないレコードなどで暗号化される前の平文テキスト値を返していた。

Post.encrypts :body

post = Post.create!(body: "Hello")
post.ciphertext_for(:body)
# => "{"p":"abc..."

post.body = "World"
post.ciphertext_for(:body)
# => "World"

このコミットは、ciphertext_forが常に暗号化済み属性のテキストを返すように修正する。

Post.encrypts :body

post = Post.create!(body: "Hello")
post.ciphertext_for(:body)
# => "{"p":"abc..."

post.body = "World"
post.ciphertext_for(:body)
# => "{"p":"xyz..."

同PRより


つっつきボイス:「例のActive Record暗号化機能ですね」「暗号化されていない値が返されていたのは明らかにバグ」

参考: Rails API ciphertext_for -- ActiveRecord::Encryption::EncryptableRecord

Rails 7のActive Record暗号化機能(翻訳)

🔗 save後の不要なserialize呼び出しを回避

レコードをsaveすると、レコードの属性ごとにActiveModel::Attribute#value_for_databaseを呼び出し、value_for_databaseserializeを呼び出す。レコードの保存が成功すると、ActiveModel::Attribute#forgetting_assignmentがその属性をリセットするが、ここでもvalue_for_databaseが呼び出される。つまり属性はsaveの直後に不要なserializeを再実行していることになる。

このコミットは、value_for_databaseをメモ化することでsave後にserializeが2回呼び出されないようにする。valueは真の情報の唯一の根拠だが、その場で変更される可能性があるため、このメモ化ではvalueがメモ化済みの@value_for_databaseと一致しない場合は慎重にチェックするようにしている。

これによってsaveのパフォーマンスが少し向上し、ほとんどの型でvalue_for_database読み出しを繰り返すときのパフォーマンスが大幅に向上する。
同PRより


つっつきボイス:「こちらは最適化で、項目によっては倍以上速くなってるものもありました」「serializeは呼び出される頻度が多いので最適化が効きそう👍」

🔗 ActiveRecord::QueryMethods#reselectにもカラムやエイリアスを含むハッシュを渡せるようになった

動機/背景
最近#45612ActiveRecord::QueryMethods#selectにハッシュを渡せるようになった。このプルリクでは、ActiveRecord::QueryMethods#selectと同じ要領でActiveRecord::QueryMethods#reselectにもハッシュを渡せるようにする。
詳細
@alextruemanが#45612#selectに導入した機能と同様に、#reselectメソッド内でカラムやエイリアスを含むハッシュを使える機能を追加する。
同PRより


つっつきボイス:「この間ActiveRecord::QueryMethodsselectにハッシュが渡せるようになっていましたね(ウォッチ20220926)」「改修はたった1行なんですね↓」「reselectの引数の形式をselectと同じにするために、ここではselectのときに追加されたprocess_select_argsを呼んでいます」「なるほど」

# activerecord/lib/active_record/relation/query_methods.rb#407
    def reselect(*args)
      check_if_method_has_arguments!(__callee__, args)
+     args = process_select_args(args)
      spawn.reselect!(*args)
    end

参考: Rails API reselect--ActiveRecord::QueryMethods`

🔗Rails

🔗 Railsで使えるminitest検証メソッドとAPIドキュメント


つっつきボイス:「jnchitoさんの記事です」「assert_in_deltaという差分チェック用のアサーションがあるのか」「assert_in_epsilonもあるんですね」「refuteよりもassert_notを使うことが多いけど、refute_nilとかは使いやすい」

参考: §2.6 利用可能なアサーション -- Rails テスティングガイド - Railsガイド

refute: 論破する、誤っているまたは不正確であると判明する

「複雑なモデルをテストするときはRSpecの方が記述の自由度は高いけど、単純なテストはminitestが書きやすくて好きです」

🔗 リアルタイム化の落とし穴


つっつきボイス:「Evil Martiansの中の人が今年5月のRailsConfで発表したスライドだそうです」「Action CableのPub/Sub周りの話ですね」「realtime-ificationという言葉がすごい」

参考: RailsConf on Notist
参考: Action Cable の概要 - Railsガイド

「あっさり目のスライドですね」「動画で見る前提なのかも」「Hotwireも使っているみたいですね」「HotwireとAction Cableの組み合わせは個人的にかなりよくできていると思っているので、もっと評価されて欲しい気持ち」「ですよね」

「HotwireとAction Cableの組み合わせはよいと思うんですが、サーバーのコードを書く人もブラウザのWebSocketを意識しておかないとデータ漏えいにつながったりするという面もあるので、正しく書くには結局RailsとJavaScriptを両方知っておく必要がありますね」「なるほど」「Hotwireの普及が遅いのはRubyとJavaScriptという2つの言語を書きたくないという気持ちもあるのかも🤔」「それもわかります」「昔ながらのマルチページアプリケーションは枯れていて安心感があるけど、Hotwireという解もあっていいと思います」

参考: WebSocket API (WebSockets) - Web API | MDN

「お、AnyCableの新バージョンも近々出るらしい↓」

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

「締めくくりの言葉が"安直な抽象化に惑わされるな"、"ツールを理解して落とし穴を避けよう"↓」


後で動画を見つけました↓。

🔗 書籍『Gradual Modularization for Ruby and Rails』


つっつきボイス:「まだ執筆中ですが、この間取り上げたShopifyのpackwerk(ウォッチ20220920ウォッチ20201005)をフィーチャーした書籍だそうです」「段階的なモジュール化か」

Shopify/packwerk - GitHub

「目次を見た感じでは俺たちが本当に欲しかったもの感あって期待できそう」「目次の"Enforce Visibility"や"Hide ActiveRecord"っていい言葉👍」「とりあえずポチってみたら進捗70%で170ページぐらいか、後で読んでみよう」

私も後でポチってみました↓。epubとpdfの両方をダウンロードできます。

🔗 その他Rails


つっつきボイス:「10年続くのってやっぱ凄い」「フィンランドのヘルシンキにある最初のrailsgirls.comも2010年からやっているそうなので、それに次ぐ歴史の長さですね」

参考: Rails Girls -- railsgirls.com

以下はつっつき後に見つけたツイートです。


前編は以上です。

バックナンバー(2022年度第4四半期)

週刊Railsウォッチ: Ruby 3.2のData.define、RubyPrize 2022最終ノミネート、Puma-dev gemほか(20221026後編)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。