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

週刊Railsウォッチ:(20220510前編)Active RecordにPromiseと非同期集計メソッドがマージ、climate_control gemほか

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

🔗 ActionView::HelpersERB::UtilのXSS修正と保護の追加

タグヘルパー内のタグ名や属性名で特定の危険な文字をエスケープし、XML仕様に沿うようにする。
:escape_attributesオプションを:escapeにリネームしてタグ全体に適用することでシンプルにした。
Álvaro Martín Fraguas
同Changelogより


つっつきボイス:「タグ名や属性名でXSSできたということ?」「どうやらちょうど今日(編注: 2022/4/28)出したセキュリティ記事↓の脆弱性修正かな」「たしかにこの修正に含まれていますね」

Railsセキュリティ修正7.0.2.4、6.1.5.1、6.0.4.8、5.2.7.1がリリースされました

参考: [CVE-2022-27777] Possible XSS Vulnerability in Action View tag helpers - Security Announcements - Ruby on Rails Discussions

「上のセキュリティ情報によると、修正前は以下のmalicious_inputでタグ名や属性名に危険な文字を渡せていたらしい」

check_box_tag('thename', 'thevalue', false, aria: { malicious_input => 'thevalueofaria' })

🔗 マルチプルデータベースのリゾルバでreading_request?を定義

  • DatabaseSelector::Resolverreading_request?をオーバーライド可能にする
    デフォルトの実装では、リクエストがget?head?かどうかをチェックするが、このプルリクによって任意のものに変更できる。メソッドがtrueを返す場合はResolver#readが呼び出され、そのリクエストがreplicaデータベースで扱われることを示す。
    Alex Ghiculescu
    同Changelogより

つっつきボイス:「reading_request?はHTTPリクエストが読み取り専用であるかどうかをミドルウェアレベルで判別できるメソッドのようですね」「replicaデータベースにPOSTやPUTやDELETEのような更新系リクエストを送信したい場合が想定されているようですね: プルリクによると、たとえばGraphQLは読み取りのクエリだけを含むリクエストであってもHTTP POSTでリクエストしますけど、内部で読み込みしか行わないならwriterではなくreplicaで処理したいといったケースで有効とある、たしかに」「通常はwriteメソッドがミドルウェアレベルで阻止されるのね」

reading_request?は新たに追加されたメソッドだから、既存の挙動が変わるわけではなさそう」「通常と違う使い方をしたい人はこれでカスタマイズできますね」

# 同PRより
def reading_request?(request)
  graphql_read = request.post? && request.path == "/graphql" && !request.params[:query]&.include?("mutation")

  graphql_read || super
end

🔗 Railsにプルリクを出すときのコツ

「ところで、Railsの機能を拡張するプルリクを送るときは、機能を直接拡張するよりも、機能を拡張可能にするインターフェイスだけ作って、使いたい人だけが機能を拡張できる形にする方がレビューに通りやすいよという話を以前聞いたことがあります」「たしかに既存の機能に影響しそうな機能追加はマージされにくいかも」「使う側がインターフェイスを拡張しなければ今までどおりという形の方がプルリクが通る可能性が高まるでしょうね」

🔗 ActiveSupport::Logger.new:formatterキーワード引数が効くようになった

Ruby標準ライブラリのLogger::newは、常に:formatterキーワード引数を渡してロガーのフォーマッタを指定できる。従来のActiveSupport::Logger.newは、常にActiveSupport::Logger::SimpleFormatterのインスタンスがフォーマッタとして設定されていたので:formatterキーワード引数が無視されていた。
Steven Harman
同Changelogより

参考: class Logger (Ruby 3.1 リファレンスマニュアル)


つっつきボイス:「なるほど、今まではロガーをnewするときに:formatterオプションに何を渡してもSimpleFormatterが使われていたのね」「||=を足すだけのシンプルな修正👍」

# activesupport/lib/active_support/logger.rb#L79
    def initialize(*args, **kwargs)
      super
-     @formatter = SimpleFormatter.new
+     @formatter ||= SimpleFormatter.new
    end

「このオプション使ったことないんですけど、他にもログフォーマッタがあるんですか?」「JSONフォーマッタとかいろいろありますよ」

🔗 一般的な非同期クエリに使えるPromiseをActive Record APIに追加

#41372の続き
修正されるissue: #44169
Relation#load_asyncを実装したときに必要性に気づいていたが、考える時間が必要だったので後回しにしていた。

現在のActive Recordにおける非同期サポートは「コレクションの結果」に限定されているが、countsumなどの集計や手作りのfind_by_sqlのようなあまり速くないクエリには、非同期のメリットを受けられるものもある。
load_asyncは、APIとして追加しやすかった(Relationはコレクションとして振る舞うので、これがイテレートされるときはAPI全体の互換性を維持しつつ単にブロックするだけで良かった)。
集計やfind_by_sqlについては、非同期モードで別のものを返す独自のAPIにするしかない。
以下の概念実証は、Relation#countこのAPIがどんなふうになるかを示すためのものである。

Post.where(published: true).count # => 2
promise = Post.where(published: true).async_count # => #<ActiveRecord::Promise status=pending>
promise.value # => 2

しかしこのAPIはあらゆる集計メソッドにも、単一レコードを返すあらゆるメソッドにも、Relation以外のすべてにも適用されるべき。
同PRより


つっつきボイス:「お〜、Active RecordにPromiseが入るのね」「async: trueみたいな指定ができるようになるのか↓」「そういえばselect_oneには生SQLを渡すこともできますね」

# activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L73
-     def select_one(arel, name = nil, binds = [])
-       select_all(arel, name, binds).first
+     def select_one(arel, name = nil, binds = [], async: false)
+       select_all(arel, name, binds, async: async).then(&:first)
      end

「だいぶ改修のボリューム大きいですね」「async_countasync_averageみたいなメソッドも追加されてる↓」「お〜、カウントはよく使われるけど重いときは重いので、ニーズはありそう」

# activerecord/lib/active_record/relation/calculations.rb#56
    def async_count(column_name = nil)
      async.count(column_name)
    end
# activerecord/lib/active_record/relation/calculations.rb#68
    def async_average(column_name)
      async.average(column_name)
    end

「このプルリクはShopifyからですね、さすが」「Shopifyなら既に使ってるかも」「今までより非同期処理がやりやすくなりそう👍」「コネクションプールが足りなくなったりしそうですけどね」

🔗 ドキュメント: Railsアップグレードガイドのcookieローテータを更新


つっつきボイス:「ドキュメント更新です」「へ〜、cookieローテータでencrypted cookieとsigned cookieを別々に設定できるのか」

# config/initializers/cookie_rotator.rb#379
Rails.application.config.after_initialize do
  Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
-   salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
+   authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
+   signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt
+
    secret_key_base = Rails.application.secret_key_base

    key_generator = ActiveSupport::KeyGenerator.new(
      secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
    )
    key_len = ActiveSupport::MessageEncryptor.key_len
-   secret = key_generator.generate_key(salt, key_len)

-   cookies.rotate :encrypted, secret
+   old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len)
+   old_signed_secret = key_generator.generate_key(signed_cookie_salt)
+
+   cookies.rotate :encrypted, old_encrypted_secret
+   cookies.rotate :signed, old_signed_secret
  end
end

🔗Rails

🔗 RailsとReactでCRUDアプリを作る(Ruby Weeklyより)


つっつきボイス:「RailsとReactでCRUDアプリですか」「ReactだけどSPA的なことはこの記事ではやってないみたい」

「Reactを使うためにrails newでesbuildを指定して、Webpackerの後継であるShakapackerを使ってる↓」「Rails 7のフロントエンド周りはいろんな人がいろんなことを試している感ありますね」

参考: esbuild - An extremely fast JavaScript bundler

Rails: Webpacker v5からShakapacker v6へのアップグレードガイド(翻訳)

「かなり長い記事だけど追ってみる価値ありそうですね」「書くのに時間かかったでしょうね...」「Rails 7のimportmap-rails↓を使わずに、Rails 6.1時代の方法でフロントエンドをやりたいときに参考になりそう👍」

Rails 7: importmap-rails gem README(翻訳)

🔗 dry-schemaは過小評価されている (Ruby Weeklyより)

dry-rb/dry-schema - GitHub


つっつきボイス:「Hanamiの技術ブログです」「dry-rbシリーズはHanamiでも結構使われていますね: 個別のgemが大きくならないように設計しているあたりの気持ちはわかる」

参考: dry-rb - Home
参考: Hanami | The web, with simplicity

「dry-rbはいぜんVirtusだったんでしたっけ?」「そうそう、Virtusの作者がdry-rbシリーズを作って、その後Virtusは開発が終了した」「Virtusのリポジトリを見てもそうなってますね」「Virtusが大きくなりすぎたのでdry-rbでは複数のgemに分割されましたね」

solnic/virtus - GitHub

「dry-rbシリーズはRailsなどと強く結合していないので、少し大きめのAWS Lambda関数を書くときとかに便利そう👍」

🔗 climate_control: 環境変数を一時的に変更するgem(Ruby Weeklyより)

thoughtbot/climate_control - GitHub


つっつきボイス:「thoughtbotのgemなんですね」「なるほど、ClimateControl.modifyのブロック内でだけ環境変数を変更したりできるらしい↓」「お〜」

# 同リポジトリより
ClimateControl.modify CONFIRMATION_INSTRUCTIONS_BCC: 'confirmation_bcc@example.com' do
  sign_up_as 'john@example.com'

  confirm_account_for_email 'john@example.com'

  expect(current_email).to bcc_to('confirmation_bcc@example.com')
end
# 同リポジトリより
require 'spec_helper'

describe Thing, 'name' do
  it 'appends ADDITIONAL_NAME' do
    with_modified_env ADDITIONAL_NAME: 'bar' do
      expect(Thing.new.name).to eq('John Doe Bar')
    end
  end

  def with_modified_env(options, &block)
    ClimateControl.modify(options, &block)
  end
end

「テストで環境変数を変更しながらテストするコードを書くときに、beforeの中で環境変数を変更すると自動では元に戻らないという問題があるじゃないですか」「そうそう、よくやっちゃいます😅」「これを踏むと"たまに落ちる"テストになってしまうヤツ」

「テストでの環境変数切り替えをちゃんとやろうとすると複雑になりがちなんですよ: 環境変数を一度どこかに退避しないといけなかったり、初期状態で未設定だった環境変数は終わった後で正確に未設定の状態に戻さないといけなかったり」「まさに」

「climate_controlを使うとそういうのを回避できるということなんでしょうね」「これはよさそう」「きっと俺たちが欲しかったもの」「覚えてたら使ってみよう👍」


「ところでclimate_controlという名前で、この記事↓でも有名なCode Climateを思い出した」「同じく」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

🔗 maybe_later: Rails向けのシンプルな非同期ジョブgem(Ruby Weeklyより)

testdouble/maybe_later - GitHub


つっつきボイス:「こちらはtestdoubleのgem」「maybe laterだから、たぶん後で?」「ジョブキューを使わずにタスクを手軽に非同期実行する感じですね」「HTTPリクエスト周りの話が中心なので、汎用的な非同期gemというよりRailsから使う前提みたい: gemspecを見るとconcurrent-rubyやRailsのrailtiesに依存しているし、spec.summaryにはRackのレスポンスやRailsのアクションが完了したらコードを実行すると書かれてる」「なるほど」

# 同リポジトリより
MaybeLater.run {
  AnalyticsService.send_telemetry!
}

「READMEを見るとこんなことが書いてある↓: 外部のジョブキューなどに依存しない分、コードが何かで終了に失敗する可能性がまれにあるのかも」

maybe_laterという名前の意味がよくわからない方へ: これは自分のコードのafter-actionコールバックが確実に動くようにするだけのgemです。呼び出すコードが重要な場合は、sidekiqなど別のものをお使いください。
同リポジトリより

「お、こういうふうにスレッドセーフでないコードも扱えるらしい↓」

# 同リポジトリより
MaybeLater.run(inline: true) {
  # スレッドセーフでないコードをここに書く
}

前編は以上です。

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

週刊Railsウォッチ: RubyのGCコンパクション改修、jemalloc、ReDoSの自動検出修正ほか(20220419後編)

今週の主なニュースソース

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

Rails公式ニュース

Ruby Weekly


CONTACT

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