Skip to content

Instantly share code, notes, and snippets.

@willnet
Last active September 5, 2024 05:55
Show Gist options
  • Save willnet/3cc38e20d44fb821209cb8a93c1310a5 to your computer and use it in GitHub Desktop.
Save willnet/3cc38e20d44fb821209cb8a93c1310a5 to your computer and use it in GitHub Desktop.
Rails7.2のマイナーフィーチャーで気になったところ

これはなに

  • 現時点(2024/08/30)でRails7.2.1がリリースされています
  • Ruby on Rails 7.2 Release Notes — Ruby on Rails Guides にメジャーフィーチャーが書かれている
  • マイナーフィーチャーはよくわからないので、個人的に気になったマイナーフィーチャをまとめています

Railties

rails bootコマンドの追加

backtraceでRails内部のコードを省略しないための環境変数が追加された

デフォルトで本番環境ではサーバのpidファイルが作られないようになった

  • Do not generate pidfile in production environments by hschne · Pull Request #50644 · rails/rails
  • rails newしたときに生成されるpumaの設定が変更された
  • Docker環境でOOMにアプリケーションサーバが殺されて再起動するときにpidファイルが残っていて起動できず、手動で削除するというオペレーションが発生するのを避けたいという話
  • これを解決するためにdevelopment環境だけpidファイルを作るという設定になった
    • Docker環境である、ないに関わらずdevelopment環境以外では作らない
    • 環境変数ENV["PIDFILE"]で明示的にpidファイルを設定することはできるので、もしpidファイルが必要なプロジェクトがあったらPIDFILEを設定しておくと良さそう
      • PIDFILEによるpidファイルの指定はRails7.1でも有効

rails cしたときのプロンプトのデフォルトが変更された

my-app(dev)>

Active Support

ErrorReporterにメッセージを送るためのメソッドが生えた

Sentry.capture_message(  
  "User's email wasn't provided",  
  level: :warning  
)
  • これもErrorReporterで対応できたらいいな〜と思っていたけど、"ErrorReporter"なのに文字列だけ送りたいメソッド生やすの無理かな…となって諦めていた
  • そこでこのPR
  • 差分を見るとわかるように「ここに来ることは想定していないなんですよ」という状況を例外としてRuntimeErrorオブジェクトを作り、本番環境ではreportメソッドを呼び出している
    • 開発環境やテスト環境では例外にしている
  • なので「ここに来ることは想定していないなんですよ」以外で文字列を送りたい場合は引き続きSentryなどが提供しているやつを引き続き直接使う必要はある
    • が、ほとんどのケースはこれだと思うので直接Sentryなどを参照することはほぼなくなるはず…
  • うまいことやったな、という印象

ログから、特定のアクションでクエリが何回実行されたかすぐ確認できるようになった

# Before
Completed 200 OK in 3804ms (Views: 41.0ms | ActiveRecord: 33.5ms | Allocations: 112788)

# After
Completed 200 OK in 3804ms (Views: 41.0ms | ActiveRecord: 33.5ms (2 queries, 1 cached) | Allocations: 112788)

CurrentAttributesにデフォルト値を設定できるようになった

class Current < ActiveSupport::CurrentAttributes
  attribute :counter, default: 0
end
  • CurrentAttribute使いには朗報では

ActionView

rails-ujsのサポートがついに終了

部分テンプレートで自動でアサインされるローカル変数を、strict locals時に無視するようになった

  • Ignore implicit locals if not declared by templates with strict locals · rails/rails@110206a
  • Rails7.1では Allow templates to define which locals they accept by joelhawksley · Pull Request #45602 · rails/rails で部分テンプレート中に使うローカル変数をマジックコメントで宣言することができるようになった
    • めっちゃべんり
  • なんだけど、例えば render @posts のようにコレクションを渡したときに困る
    • コレクションをレンダリングする際には、例えば↑の例であればpost_counter, post_iterationというローカル変数が自動的に使えるような仕様になっているのだった
      • これに引っかかって意図せずエラーになってしまう
    • counterもiterationも、何番目にレンダリングされたかを表すもの
    • counterは単純に数値が入っている(後方互換性のために残されている)
    • iterationはfirst?last?size?などが生えていて高機能
  • なのでこれらのローカル変数をstrict localで指定しない限り部分テンプレートに渡さないようになった

Action Pack

IPアドレスによるアクセス制限をかける機能の追加

Active Record

mariadbでもINSERT RETURNINGが使えるぞ

モデルのinspectの結果が変わる

Post.first.inspect #=> "#<Post id: 1>"

attributes_for_inspectをゴニョると挙動を変更できる

Post.attributes_for_inspect = [:id, :title]
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!">"

全部表示したいぞ、というときは:allにする

Post.attributes_for_inspect = :all
Post.first.inspect #=> "#<Post id: 1, title: "Hello, World!", published_at: "2023-10-23 14:28:11 +0000">"
  • 開発環境とテスト環境ではデフォルト:all
  • 一時的に全部表示したいとき用にfull_inspectメソッドも用意されている
  • これ厳密には非互換な変更なんじゃないの、という気がするけどとくに段階的移行のためのなにかはない
    • まあinspectの振る舞いに依存しているコードとか普通ないやろ、というのはわかる

ARのexplainメソッドの適用範囲が広がった

複数形のinverse_ofが自動推測される

Active Support Instrumentationでクエリの行数を取得できるようになるぞ

query_log_tagsでクエリを実行した場所を追加できるようになった

  • Support :source_location tag option for query log tags by fatkodima · Pull Request #50969 · rails/rails
  • query_log_tagsはクエリ実行時のコメントにどのコントローラやアクションから実行されたやつなのかをコメントで追記してくれるやつ
  • 前は basecamp/marginalia: Attach comments to ActiveRecord's SQL queries というgemだった
  • marginaliaにはクエリを実行した場所を出力する機能はあったのだけど、query_log_tagsに移行する際にその機能は入らなかった
  • "実行した場所を表示する"のが結構重たい処理だったというのが理由の一つ
    • これまではcallerとかcaller_locationというメソッドを利用して呼び出し元のスタックトレースを全部取得する必要があった
    • Ruby3.2からeach_caller_locationというメソッドが追加されて、これを利用することで処理の重たさを軽減できるようになった
      • Thread#each_caller_location
      • 一つずつバックトレースをイテレーションしていくので、途中でお目当てのものが見つかったらbreakできる
    • これにより導入してもいいんじゃない?となったみたい
    • オプションとしてconfig.active_record.query_log_tags << :source_location のような設定をすると有効になる
    • 軽減したとはいえまだ重たいので基本的には開発環境で使え、とのこと
      • もしくは本番環境でデバッグしたいときだけオンにするとか

select, order, pluckでjoinでJOINしたテーブルのカラム指定をハッシュでできるようになった

Topic.includes(:posts).order(posts: { created_at: :desc })
# Before
Post.joins(:comments).pluck("posts.id", "comments.id", "comments.body")

# After
Post.joins(:comments).pluck(posts: [:id], comments: [:id, :body])
Post
  .joins(:comments)
  .select(
    posts: { id: :post_id, title: :post_title },
    comments: { id: :comment_id, body: :comment_body}
  )

# instead of:

Post
  .joins(:comments)
  .select(
    "posts.id as post_id, posts.title as post_title,
    comments.id as comment_id, comments.body as comment_body"
  )

既存のテーブルにcounter cacheを導入する時のツラミを軽減するオプションが入った

  • Add the ability to ignore counter cache columns while they are backfilling by fatkodima · Pull Request #51453 · rails/rails
  • 既存のテーブルにcounter cacheを導入すると、counter cacheのカラムに現在の値を反映させる作業が必要になる
  • sizeメソッドなどの一部の関連メソッドはcounter cacheが有効だとそのカラムの値を使うケースがあるので、現在の値を反映するまでは有効にできない
  • カラム追加→rakeタスクなどで↓のように更新する→counter cacheをオンにする、というのがパッと思いつくけど、rakeタスクからcounter cacheをオンにするまでにcommentsの増減があったら…?
# 雑に考えた実装です
Post.in_batches do |relation|
  relation.pluck(:id).each do |id|
    Post.reset_counters(id, :comments)    
  end
end
  • なのでこれまで正しくやりたい場合はCommentにコールバックを仕掛けるか、トリガを仕掛けるかくらいしか方法がなかった
  • これからは次のように設定できる
class Comment < ApplicationRecord
  belongs_to :post, counter_cache: { active: false }
end
  • ↑のようにすると、sizeメソッドなどはcounter cacheのカラムは使わない
  • しかし対象モデルの増減時にはcounter cacheのカラムを増減させる
  • ので、rakeタスク実行で問題なくなるのだった

ついにrecursive CTEがサポートされたぞ

Post.with_recursive(
  post_and_replies: [
    Post.where(id: 42),
    Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id'),
  ]
)
WITH RECURSIVE "post_and_replies" AS (
  (SELECT "posts".* FROM "posts" WHERE "posts"."id" = 42)
  UNION ALL
  (SELECT "posts".* FROM "posts" JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id)
)
SELECT "posts".* FROM "posts"
  • SQLアンチパターンで記載されているナイーブツリーは、これからはアンチパターンではなくなるかもしれないですね
    • とはいえ経路列挙モデルなどのほうが適しているケースが有る(SELECT時の速度面)のでケースバイケースで一番いいのを選ぶ、というのは変わらない

touch_allがin_batches対応になった

Post.in_batches.touch_all

テスト

クエリの実行回数を簡単にテストでチェックできるテストヘルパが追加された

class ArticleTest < ActiveSupport::TestCase
  test "queries are made" do
    assert_queries(1) { Article.first }
  end
end

assert_diffrerenceとassert_changesのエラーメッセージの改善

次のようなテストケースがある

test "see proc output" do
  assert_difference -> { 1 } do
  end
end

do...endを実行したあとに-> { 1 }に変化があることを確認しているが、当然-> { 1 }は毎回1を返すので失敗する。このときこれまではこのようなメッセージで失敗していた

#<Proc:0x000074357846eae8 /home/richard/my-test-app/test/models/test.rb:21 (lambda)> didn't change by 1, but by 0.
Expected: 2
  Actual: 1

これがこうなる

"-> { 1 }" didn't change by 1, but by 0.
Expected: 2
  Actual: 1

Rails標準のテスト並列実行時にDBのTRUNCATEをスキップできるようになった

  • Add ENV["SKIP_TEST_DATABASE_TRUNCATE"] flag to speed up multi-process test runs by dhh · Pull Request #51686 · rails/rails
  • Rails標準のテストで、かつ並列実行しているときの振る舞いを変えたという話
  • HEYだと24プロセスでそれぞれ178テーブルをTRUNCATEしていた
    • 24*178=4272
    • それをやめたら最大10秒テスト実行時間が短縮されたとのこと
  • テストのデータが全部トランザクションの中で作られているならTRUNCATEは不要
  • もちろんトランザクション外でマスターデータなどを入れていたら、テスト実行前に一回消しておくのが安全ですね

assertionが一つもないテストを検知する仕組みがはいった

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment