- Ruby / Rails関連
週刊Railsウォッチ: 2022年のRails振り返り記事、RailsにDocker関連ファイルが追加ほか(20230125前編)
こんにちは、hachi8833です。週刊Railsウォッチがほぼ1か月ぶりのご無沙汰となりました 🙇
🔗Rails: 先週の改修(Rails公式ニュースより)
だいぶ間が空いてしまいましたので、昨年末の改修から追いかけていきます。
🔗 all_queries: true
を指定したデフォルトスコープだけがreload
時にクエリに適用されるよう修正
修正対象: #46731
現在のreload
の振る舞いは、いずれかにall_queries: true
を指定するとすべてのデフォルトスコープが適用されるようになっている。reload
するときは、all_queries: true
を指定したデフォルトスコープだけが適用されるのが正しい振る舞い。
同PRより
概要
現在のdefault_scopesの振る舞いに一貫していない部分がある。
Active Recordモデルには多くのスコープを設定でき、レコードをリロードするときはどのスコープも適用されない。スコープのどれかひとつにall_queries: true
が指定されていると、すべてのスコープが適用される。
参考: rails/persistence.rb at main · rails/rails
参考: rails/persistence.rb at main · rails/rails再現手順
1 - あるActive Recordモデルに
default_scope(all_queries: false { where("1=1") }
を追加する
2 - レコードを作成し、クエリをかけ、リロードする: このスコープはリロードクエリ内で適用されない
3 - 今度はall_queries: true
を指定してdefault_scope(all_queries: true { where("2=2") }
という別のデフォルトスコープを追加する
4 - 手順2を繰り返してクエリを調べる: 両方のスコープが適用される# frozen_string_literal: true require "bundler/inline" gemfile(true) do source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem "rails", github: "rails/rails", branch: "main" gem "sqlite3", "< 1.5" end require "active_record" require "minitest/autorun" require "logger" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.logger = Logger.new(STDOUT) ActiveRecord::Schema.define do create_table :posts, force: true do |t| t.boolean :deleted end end class Post < ActiveRecord::Base default_scope { where(deleted: false) } end class BugTest < Minitest::Test def test_default_scope_side_effect post = Post.create! last_query = nil ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, data| last_query = data[:sql] end expected_query = 'SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?' # デフォルトスコープは適用「されない」 post.reload assert_equal expected_query, last_query # 何もしないデフォルトスコープ: `all_queries: false` Post.default_scopes << ActiveRecord::Scoping::DefaultScope.new(lambda { where("1 = 1") }, false) # クエリは変更されず、デフォルトスコープはまったく適用されない post.reload assert_equal expected_query, last_query # 何もしないデフォルトスコープ: `all_queries: true` Post.default_scopes << ActiveRecord::Scoping::DefaultScope.new(lambda { where("2 = 2") }, true) expected_modified_query = 'SELECT "posts".* FROM "posts" WHERE (2 = 2) AND "posts"."id" = ? LIMIT ?' # 3つのスコープがすべて適用される post.reload assert_equal expected_modified_query, last_query end end
期待される振る舞い
Active Recordモデルにdefalut_scope
を追加したとき、レコードをリロードするときに他へのdefault_scope
の振る舞いに影響すべきではない。
クエリの振る舞いを一貫させるとしたら、以下のどちらかを期待したい。
- モデルをリロードしたときに、
all_queries
を指定したデフォルトスコープだけがクエリに適用される- モデルをリロードするときは
default_scope
を適用しない(こういうスコープがreload
操作で適用される理由が自分にはさっぱりわからない)実際の振る舞い
モデルにはall_queries: false
のdefault_scopesを複数定義可能で、レコードをリロードするときはどれも適用されない。
そういうモデルにall_queries: true
を指定したdefault_scopeが1個あると、すべてのdefault_scopeが適用されてしまう。
#46731より
つっつきボイス:「あるデフォルトスコープでall_queries: true
を指定したときに他のデフォルトスコープにもall_queries: true
が効いてしまっていたのを修正したということらしい」「ちなみにAPIドキュメントを見るとall_queries
のデフォルトはnil
なので普段は無効なんですね」
参考: Rails API default_scope
-- ActiveRecord::Scoping::Default::ClassMethods
🔗 productionでRAILS_MASTER_KEY
を渡さなくてもassets:precompile
を実行可能にした
production環境でイメージビルド手順中にアセットをコンパイルするときに本物の
RAILS_MASTER_KEY
を渡さなければならないのは不便。そこで、ENV["SECRET_KEY_BASE_DUMMY"] = 1
を指定すれば、development環境やtest環境でやっているのと同様にダミーのsecret_key_base
を渡せるようにする。この場合、本物のcredentialやメッセージダイジェスト検証にはアクセスできなくなるが、実際のところは無用なので、ビルドを完了できるようになる。
同PRより
つっつきボイス:「DHH自らのプルリクです」「なるほど、たしかにマスターキーを指定しなくてもアセットをプリコンパイルしたいですよね」「ということは今まではマスターキーがないとプリコンパイルできなかったんですか?」「プリコンパイルそのものでは不要でも、rails assets:precompile
コマンドのどこかの処理でマスターキーを要求していた箇所があってそこで止まってたんでしょうね」「地味に不便ですね」「そもそもマスターキーは不必要な場所には絶対配布したくないものなので、ビルドするだけのサーバーとかには置きたくない」「たしかに」
# railties/lib/rails/application.rb#473
def secret_key_base
- if Rails.env.development? || Rails.env.test?
+ if Rails.env.development? || Rails.env.test? || ENV["SECRET_KEY_BASE_DUMMY"]
secrets.secret_key_base ||= generate_development_secret
else
validate_secret_key_base(
ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base
)
end
end
参考: § 4.1 アセットをプリコンパイルする -- アセットパイプライン - Railsガイド
🔗 Action Textで変更のないノードの置き換えを回避して高速化
置き換えのロジックが何らかの条件付けに基づいている場合のパフォーマンスを改善し、スキップしたい場合は無変更のノードをそのまま返す。以下のような感じ:
content.fragment.replace("p") do |node| if node.text =~ /replace me/ "<p>replaced</p>" else node end end
(ベンチマークコードは省略)
結果:Warming up -------------------------------------- Current implementation 2.000 i/100ms New implementation 5.000 i/100ms Calculating ------------------------------------- Current implementation 32.484 (±30.8%) i/s - 134.000 in 5.036419s New implementation 74.878 (±38.7%) i/s - 250.000 in 5.052168s Comparison: New implementation: 74.9 i/s Current implementation: 32.5 i/s - 2.31x (± 0.00) slower
つっつきボイス:「Action Textで不要なノードの置き換えを回避する形での最適化👍」「if
文が追加された部分がそれなんですね↓」「ベンチマークが倍速くなってる」
# actiontext/lib/action_text/fragment.rb#37
def replace(selector)
update do |source|
source.css(selector).each do |node|
- node.replace(yield(node).to_s)
+ replacement_node = yield(node)
+ node.replace(replacement_node.to_s) if node != replacement_node
end
end
end
参考: Action Text の概要 - Railsガイド
🔗 Active SupportのMessageEncryptorsとMessageVerifiersにtransitional
属性を追加
- PR: Add
Message{Encryptors,Verifiers}#transitional
by jonathanhefner · Pull Request #46755 · rails/rails
このコミットは
ActiveSupport::MessageEncryptors
とActiveSupport::MessageVerifiers
にtransitional
属性を追加する。transitional = true
を設定すると、MessageEncryptorsやMessageVerifiersのビルド時にローテーションの最初の2件をスワップする。
たとえば、以下のコンフィグではMessageVerifiersがserializer: Marshal, url_safe: true
でメッセージを生成し、生成されたメッセージを3つのオプションのいずれかを用いて検証できるようになる。verifiers = ActiveSupport::MessageVerifiers.new { ... } verifiers.rotate(serializer: JSON, url_safe: true) verifiers.rotate(serializer: Marshal, url_safe: true) verifiers.rotate(serializer: Marshal, url_safe: false) verifiers.transitional = true
これはアプリケーションをローリングデプロイする際に、まだ更新されていないサーバーが更新済みサーバーからのメッセージを検証可能にする必要がある場合に便利。特に、
Rails.application.message_verifiers
のデフォルトのローテーションに適用できる。
同PRより
つっつきボイス:「transitional
属性の追加は、鍵の方式を変更するときなんかに鍵のローテーションをやりやすくする改修のようですね👍」「ローリングデプロイのときに便利とありますね」「ローリングデプロイでは移行中に古い方式のサーバーと新しい方式のサーバーが共存するので、そういう状況向けでしょうね: そのうち具体的な使用例記事が出てくるかも」
参考: デプロイ / リリース 手法 まとめ - galife -- ローリングデプロイ
🔗 Rails.env.local?
でdevelopmentとtestの環境変数をまとめて扱えるようになった
test環境やdevelopment環境で実行するときに
Rails.env
をチェックすることが多いので、1個にまとめる。
従来:if Rails.env.development? || Rails.env.test?
改修後
if Rails.env.local?
同PRより
つっつきボイス:「これもDHHによるプルリクです」「if Rails.env.development? || Rails.env.test?
はよく使われる書き方ですけど、それをif Rails.env.local?
でまとめて書けるようになったのか」「地味にありがたい改修」「localはdevelopment環境とtest環境の両方を表すんですね」「こういう改修をおもむろに入れてくるのがDHHらしい」
🔗 ActiveRecord::Relation
の#none?
、#any?
、#one?
でEnumerable
と同様のパターン引数を渡せるようになった
Enumerable
版の#none?
、#any?
、#one?
述語メソッドはそれぞれブロックの代わりにオプションのパターン引数を受け取る。パターンが指定された場合、これらのメソッドはEnumerable
内にある要素とパターンがマッチするかどうかをRubyのcase
文の===
演算子でチェックする。
以下の2つは同等。products.any?(MediaBlock) products.any? { |product| MediaBlock === product }
このスタイルをポリモーフィック関連付けでも使えると便利だろう。たとえば
Order#plan?
メソッドの場合は以下のようになる。class Order has_many :line_items has_many :products, through: :line_items # orderにplanが含まれていたらtrueを返す def plan? products.any?(Plan) end end class LineItem belongs_to :order has_one :product, polymorphic: true end
しかし
ActiveRecord::Relation
版のこれらのメソッドはオプションのパターン引数を受け取らない。背後で読み込まれているrecords
配列はブロックが与えられたときにこれを行っているので、それらに委譲する形でこれらのメソッドを更新する。
同PRより
つっつきボイス:「なるほど、RubyのEnumerable
と同じようにActiveRecord::Relation
でもブロックを渡す方法に加えて#none?
や#any?
や#one?
メソッドも呼べるようになった」「コード例やテストコードを見た感じではSTIを想定している感じですね」
参考: § 5 シングルテーブル継承 (STI) -- Active Record の関連付け - Railsガイド
参考: module Enumerable
(Ruby 3.2 リファレンスマニュアル)
🔗 production環境のRailsコンソールでIRBのオートコンプリートがデフォルトで無効になった
IRB_USE_AUTOCOMPLETE=true
を設定すればこのデフォルトの振る舞いをオーバーライドできる。
Stan Lo
同Changelogより
つっつきボイス:「これだけ公式更新情報ではなく以下の記事で知りました↓」
「productionのコンソールでIRBのオートコンプリートをデフォルトでオフにするようになった: おそらくリッチなCLI描画が動かないような環境や、事前に検証済みのコードをコピペする場合なんかにオートコンプリートが干渉して想定外の挙動が起こるのを防ぐためなんじゃないかな」「最初にIRB 3.2でそのためのIRB_USE_AUTOCOMPLETE
を追加して、それからこの改修を行ったという流れのようです: ちなみにどちらもst0012さんがやっています」
「現場だとncurses(CLIのインターフェイス構築用ライブラリ)あたりがうまく動かないWebコンソールを使わざるをえないこともあって、オートコンプリートを止めないとEnterキーを叩くのも怖いなんてこともありますよね」「そうそう」
🔗Rails
🔗 2022年のRails振り返り記事
- 元記事: Ruby on Rails — This Year in Rails, a summary of 2022!(Rails公式ニュースより)
- 元記事: Year in Review 2022: Tenderlove's Ruby and Rails Reflections and Predictions(Ruby Weeklyより)
つっつきボイス:「1件目はRails公式の2022年振り返り記事で、2件目は@tenderloveさんによる振り返り記事です」「ほとんどは週刊Railsウォッチで扱ってきたものだと思いますが、こうやってまとまるのはよい👍」「@tenderloveさんの記事の末尾には、言語とフレームワークとIDEが今後もっと密に連携するようになることを期待したいとも書かれていますね」
「以下の記事は許可をいただけたので翻訳中です↓」「お、Rails 7.1が近いのかな?」「いえ、Rails 7.1はブランチもタグもできてない状態なのでまだ先だと思います」「ありゃ残念」「記事は7.1に入るであろう2022年の新機能のうち著者が気に入っているものを中心に取り上げています」「なるほど、ぼくの好きな機能まとめという感じ」「たしかに全部は入り切らないでしょうね」「週刊Railsウォッチの先週の改修をまとめたような感じです」
- 元記事: An Overview Of Ruby on Rails 7.1 Features. Part I.
- 元記事: An Overview Of Ruby on Rails 7.1 Features. Part II.
- 元記事: An Overview Of Ruby on Rails 7.1 Features. Part III. -- つっつき後に公開されました
🔗 RailsにDockerfileとdocked gemが追加された(Rails公式ニュースより)
つっつきボイス:「RailsにDockerfileが追加された下のプルリクがBPS社内Slackでも話題になっていましたが、その他にdockedというgemもRailsリポジトリに入ってきました↑」「dockedは、Railsにおけるdocker公式対応なテンプレ部分をgemに切り出したものに近い感じかな」「公式に入ったんですか」「for beginnersと書かれているので主にそういう用途なんでしょうね」「このDockerfileがそのまま実際のプロジェクトで使えるかどうかはともかく、DockerがRailsに公式に取り入れられたことでDockerの設定で議論しなくても済むようになることは期待できそう👍」
🔗 RailsのSSRF解説(RubyFlowより)
つっつきボイス:「CSRF(Cross-Site Request forgery)じゃなくてSSRF(Server-Side Request Forgery)の解説記事なんですね」「SlackにURLを貼るとOGP情報をサーバー側で取ってきて展開しますけど、ああいうふうにユーザーが入力したURLをサーバーサイドがアクセスしに行くところを攻撃するのもSSRFの一種」「そういえばはてなブックマークなんかもそういう動作ですね」「AWS EC2インスタンスはインスタンスの情報を固定URLで取れるんですが、そこを攻撃して鍵などの設定情報を盗み出したりする例でよくSSRFが引き合いに出されます」「なるほど」「SSRFもだいぶ知られてきましたが、これに限らず、ユーザーが入力するURLにサーバー側でアクセスするときは十分注意が必要」「クローラー的なことをさせると踏みがちですね」
参考: SSRF(Server Side Request Forgery)徹底入門 | 徳丸浩の日記
参考: [待望のアプデ]EC2インスタンスメタデータサービスv2がリリースされてSSRF脆弱性等への攻撃に対するセキュリティが強化されました! | DevelopersIO
🔗 Railsアプリで見落としやすい"eager loadingできてない箇所"(Ruby Weeklyより)
つっつきボイス:「N+1クエリ問題を見落としやすい場合があるという記事だそうです」
# 同記事より
class User < ApplicationRecord
has_many :products, strict_loading: true
end
「上のモデルではstrict_loading: true
でeager loadingしていて、以下のサンプルだと1つ目は正しくraiseされているけど、2つ目のようにスコープを追加するとすり抜けてN+1クエリが発生してしまうヤツですね」「踏んだらつらいヤツ」
# 同記事より
User.last(2).map(&:products).map(&:to_a)
# raises => ActiveRecord::StrictLoadingViolationError
User.last(2).map(&:products).map(&:by_rating).map(&:to_a)
# productsは読み込まれるがN+1クエリが発生する
🔗 Railsで"期間"を扱うときは要注意(Ruby Weeklyより)
つっつきボイス:「期間の扱いは定番ネタ」「日付や時刻がらみはホント難しい」「みんなUnix時間で生きていくことにすればいいのに」「うるう秒を廃止するという話も最近出てましたね」
参考: UNIX時間 - Wikipedia
参考: うるう秒、2035年までに廃止へ | ギズモード・ジャパン
🔗 その他Rails
つっつきボイス:「これはruby-jp Slackで知ったもので、discuss.rubyonrails.orgのsecurity
タグで直近のRailsセキュリティに関する議論を一覧できます」「なるほど、この間のRailsセキュリティ修正↓で扱われていたものもここで見えますね👍」
前編は以上です。
バックナンバー(2022年度第4四半期)
週刊Railsウォッチ: Ruby 3.2の正規表現高速化、Googleのosv-scannerほか(20221221後編)
- 20221220前編 RailsのRuby 3.2.0対応、ActiveSupport::Durationの暗黙の変換ほか
- 20221214後編 Ruby 3.2.0 RC1がリリース、YARVアドベント記事、ChatGPTほか
- 20221213前編 『RubyとRailsの何が強いのか』、書籍『Ruby on Railsステップアップ』ほか
- 20221207後編 JRubyが9.4.0.0でRuby 3.1に対応、IRB v1.5.0リリースほか
- 20221206前編 月刊のHotwireニュースレター、pessimize gemほか
- 20221130後編 Ruby 3.2のParser目玉機能ほか
- 20221129前編 Hanami 2.0リリース、Railsに関わる技術の体系化を目指した本ほか
- 20221122 The Rails Foundation発足、Ruby 3.2.0 Preview 3リリース、Ruby演算子クイズほか
- 20221116後編 Rubyを使っている企業の時価総額リスト、irbのshow_source、GitHub Codespacesほか
- 20221115前編 RailsチュートリアルがRails 7対応版をリリース、ViewComponentで使えるLookbookほか
- 20221102後編 書籍『Programming Ruby 3.2 (5th Edition)』、ReDoSチェックサイトほか
- 20221101前編 Packwerkの詳しい解説書『Gradual Modularization for Ruby and Rails』ほか
- 20221026後編 Ruby 3.2のData.define、RubyPrize 2022最終ノミネート、Puma-dev gemほか
- 20221025前編 rodauth-rails gem作者の解説記事、turbo-railsの有料チュートリアルほか
- 20221019後編 Ruby技術者認定試験再受験無料キャンペーン、Starlink日本で販売開始ほか
- 20221018前編 Rails向けLanguage Server “refreshing”開発中、JetBrains Fleetほか
- 20221012後編 RailsとPostgreSQLで列挙型を作成する6つの方法、Ubuntu Proほか
- 20221011前編 Turbo 7.2.0リリース、GitLabのDevSecOpsサーベイ結果ほか
- 20221004後編 ヒアドキュメント拡張の提案、『組織に自動テストを根付かせる戦略』ほか
- 20221003前編 Kaigi on Rails 2022のタイムテーブル発表、書籍『Practicing Rails』ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)