- Ruby / Rails関連
週刊Railsウォッチ:(20220510前編)Active RecordにPromiseと非同期集計メソッドがマージ、climate_control gemほか
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 ActionView::Helpers
とERB::Util
のXSS修正と保護の追加
タグヘルパー内のタグ名や属性名で特定の危険な文字をエスケープし、XML仕様に沿うようにする。
:escape_attributes
オプションを:escape
にリネームしてタグ全体に適用することでシンプルにした。
Álvaro Martín Fraguas
同Changelogより
つっつきボイス:「タグ名や属性名でXSSできたということ?」「どうやらちょうど今日(編注: 2022/4/28)出したセキュリティ記事↓の脆弱性修正かな」「たしかにこの修正に含まれていますね」
「上のセキュリティ情報によると、修正前は以下のmalicious_input
でタグ名や属性名に危険な文字を渡せていたらしい」
check_box_tag('thename', 'thevalue', false, aria: { malicious_input => 'thevalueofaria' })
🔗 マルチプルデータベースのリゾルバでreading_request?
を定義
DatabaseSelector::Resolver
のreading_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における非同期サポートは「コレクションの結果」に限定されているが、
count
やsum
などの集計や手作りの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より
- Rails API:
load_async
--ActiveRecord::Relation
つっつきボイス:「お〜、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_count
やasync_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 7のimportmap-rails↓を使わずに、Rails 6.1時代の方法でフロントエンドをやりたいときに参考になりそう👍」
🔗 dry-schemaは過小評価されている (Ruby Weeklyより)
つっつきボイス:「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に分割されましたね」
「dry-rbシリーズはRailsなどと強く結合していないので、少し大きめのAWS Lambda関数を書くときとかに便利そう👍」
🔗 climate_control: 環境変数を一時的に変更するgem(Ruby Weeklyより)
つっつきボイス:「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を思い出した」「同じく」
🔗 maybe_later: Rails向けのシンプルな非同期ジョブgem(Ruby Weeklyより)
つっつきボイス:「こちらは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後編)
- 20220418前編 RailsConf 2022が5月17〜19日開催、認可機能解説記事ほか
- 20220412後編 HashieでRubyのハッシュを強化、最近のRubyコア解説記事ほ
- 20220411前編 Turbo Railsチュートリアル、Active Recordの「Leaky Abstraction」を削減ほか
- 20220406後編 RBS関連記事、Ruby formatterプロジェクト、Google Cloud Runほか
- 20220404前編 Ruby 3.2.0 Preview 1リリース、Rails向けDocker環境ジェネレータ、scientist gemほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)