おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

Rails8.2に入る機能で気になったもの

今日(2026/10/07)時点でまだRails8.2はリリースされていませんが、今日開催のGinza.rb 第99回 でRails8.2特集をするので、これまでにRails8.2用に入った変更をまとめました。

これまでは節ごとにリンクが付与されるのが便利でgistに書いていたのですが、どうやら最近からはてなブログでも節のリンクが付与されるようになったので今回からはてなブログに書いていこうと思います。

自分が気になったものだけを書いているので抜け漏れ等あると思います。間違いもあるかもしれないので気づいたらコメントください。

今日以降で新しい機能が追加されたり変更されたりしたら追記や編集などする予定ですが、ベストエフォートでやるつもりなのでご了承ください (( ⁰⊖⁰)/)

過去のまとめ

Railties

存在しない環境名でrails sしたときにエラーになる

ローカルCIでfast failできるようになった

ローカルCIで並列実行できるようになる

  • Add parallel step groups to CI runner by djmb · Pull Request #56774 · rails/rails
  • ローカルCIにgroupというDSLが追加された
    • parallelというオプションがあり、ここで並列数を書く
      • 並列数で書いた数のスレッドでgroup内のステップを実行していく
      • 各ステップはQueueに入れられ、一つのスレッドが一つずつQueueからステップをpopして実行、という流れ
      • なので↓は3スレッドでそれぞれのstepを実行する
group "Checks", parallel: 3 do
  step "Style: Ruby", "bin/rubocop"
  step "Security: Brakeman", "bin/brakeman --quiet"
  step "Security: Gem audit", "bin/bundler-audit"
end
  • groupはネストすることができる
    • parallelを書けるのは一番外側のgroupだけ
    • 内側のgroupは並列に処理されない
    • ↓はTestsの二つのstepは順番通りに実行される
group "Checks", parallel: 2 do
  group "Tests" do
    step "Tests: Rails", "bin/rails test"
    step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant"
  end

  step "Style: Ruby", "bin/rubocop"
  step "Security: Brakeman", "bin/brakeman --quiet"
  step "Security: Gem audit", "bin/bundler-audit"
end

deprecation情報がStructured Eventに通知されるようになった

boot時にroutes.rbに対応するものがない時に警告してくれるようになる

CI実行時に rake_eager_loadが効くようになる

Chromiumのワークスペース自動検出用のエンドポイントがRailsに追加された

  • Introduce DevToolsController for chrome workspaces by coorasse · Pull Request #56245 · rails/rails
  • chromiumのワークスペース機能を使うとchromiumのdevtoolでローカルの開発ができる
  • それの設定は.well-known/appspecific/com.chrome.devtools.jsonでできる
  • .well-known/appspecific/com.chrome.devtools.jsonのルーティングを追加して、ワークスペース用のjsonを返すようになった
  • すると次のようにchrome devtoolsのSourcesからローカルのコードを編集できる
  • フロントエンドまわりのコードを編集してすぐ内容確認したいようなケースでは便利そうだなと思いました

rails statsで統計を取りたいファイルの拡張子を一つずつ追加できるようになった

Rails::CodeStatistics.pattern = /^(?!\.).*?\.(rb|js|ts|css|scss|coffee|rake|erb|slim)$/
  • ライブラリ(例: slim)が動的に拡張子を足したいときにこれだと困る(他にも追加があるかもしれない)ので、拡張子を一つ追加するメソッドregister_extensionが追加された
Rails::CodeStatistics.register_extension("slim")
  • どちらかというとライブラリ作者向けの変更

Rails.applicationをRails.appと短縮してかけるようになった

Rails.app.revision の導入

Useful in many context, deployment tracking, error reporting, monitoring, even sometimes in cache keys.

  • ↑とのことで将来的に結構使われるんじゃないかなという気がします

rails consoleがexecutorでwrapされるようになった

Rack::Sendfileがデフォルトから外れた

rails queryコマンドが追加された

  • Query command for read-only database queries by lewispb · Pull Request #57156 · rails/rails
  • railsのサブコマンドとしてqueryが追加された
  • rails query "Account.where(plan: 'premium').limit(2)"のように使うと、リードレプリカなDBに対してクエリを発行し、結果をjsonで返してくる
    • リードレプリカに相当するコネクションがない時もwhile_preventing_writesで更新系のクエリを防ぐようにはなっている
  • rails query --sql "SELECT id, name FROM accounts LIMIT 2"のようにARを通さずに直接クエリを投げることもできる
  • 他にもrails query schemaでschema全体の情報を取得する、とかrails query schema acountsのようにして特定のテーブルの定義を見る、とかrails query modelsで全モデルのメタ情報を見るとかrails queyr explainでexplainの情報を見るとかできる
  • 実際はrails runnerで実行したクエリをJSONで返すのを簡単にしたくらいの機能ではある
    • Rails公式で使えるようになることでAIエージェントが本番のデータを見て何かする、がやりやすくなるのが良い

アプリケーションコード限定でfrozen string literalをデフォルトにする

  • Enable frozen string literal by default by byroot · Pull Request #57252 · rails/rails
  • frozen string literalは古いgemではまだ対応しきれていないものがあるので、--enable-frozen-string-literalとするのはなかなか難しい
  • アプリケーションコードだけなら自分たちが管理しているので切り替えやすい
  • bootsnapに「アプリケーションコードだけfrozen string literalにする」を可能にするオプションが追加された
  • Rails8.2でrails newするとこれが設定されたコードが出力される
  • 合わせてrubocopの"StringLiteralsFrozenByDefault"が有効になる
  • だいたいのプロジェクトはStyle/FrozenStringLiteralCommentによって# frozen_string_literal: trueがついていると思うのだけど、これが有効になればマジックコメントを削除できる
    • Rails8.2を待たずともbootsnapをアップグレードして設定をするだけで有効にできる

rails plugin new以外のサブコマンドを明示的にエラーにした

i18nファイルの存在確認を遅延させてRailsの起動を早くした

rails cした時にバナーが出るようになった

rails newしたときに生成されるGitHub Actionsの各ジョブにデフォルト15分制限が追加された

publicディレクトリ配下のエラーページがGitHubの言語統計に含まれないようになった

Active Support

SecureRandom.base32が追加された

  • Add SecureRandom.base32 by monorkin · Pull Request #56288 · rails/rails
  • base32は読みやすさを重視して5ビットをそれぞれの文字列に変換するもの
  • base32の定義はいくつかあるみたいだけど、ここではCrockfordのBase32(I, L, O, Uを除く)を採用している
    • Base 32
    • 数字の方(0, 1, 8, 9)を省略するパターンもあるみたい
  • これを利用してランダムで視認しやすい文字列を生成するメソッドSecureRandom.base32が追加された
  • ワンタイムで入力するコードなどに便利

Rails標準のテストで並列にテストを実行したときの順番を固定できるようになった&テスト実行時間の平準化ができるようになった

this_week?, this_month?, this_year?メソッドが追加された

RedisCacheStoreでキャッシュを読み込むのと同時に消す、がやりやすくなった

TimeやDateのRails用変換フォーマットが追加された

  • Introduce ActiveSupport::TimeFormats and ActiveSupport::DateFormats for registering custom date formats by paracycle · Pull Request #57345 · rails/rails
  • Time.current.to_fs(:db)などとしたときに使われるフォーマットのハッシュが格納されているTime::DATE_FORMATSとDate::DATE_FORMATSがdeprecatedになった
  • これらの定数にはハッシュが入っていて、これまではそのハッシュを破壊的に変更してフォーマットの追加を行なっていたが、それが良くないやろとなった
  • 定数はimmutableであるべき
  • なので定数で保持するやり方をdeprecatedにして、新しいAPIであるActiveSupport::TimeFormats.registerおよびActiveSupport::DateFormats.registerを通じてフォーマットの追加を行えるようにする
  • 定数に依存している後方互換性を考えて、8.2では両方使えるがどこかのバージョンで定数は消される予定

Date#this_quarter?とTime#this_quater?メソッドの追加

ActiveSupport::Ractors.on_mainメソッドが追加された

class Something
  def self.something
     @ivar ||= :a
  end
end
  • こう書けばOK
class Something
  def self.something
     @ivar || ActiveSupport::Ractors.on_main { @ivar ||= :a }
  end
end

ActiveSupport::TimeZone#parseでエラーになっていなかった引数をエラーにできるようになった

Action Pack

routes.rbで:controllerと:actionは使えなくなる

get "photos", to: "photos#index"
get "photos/:id", to: "photos#show"
  • 他にも色々細かい差分がある
    • 過去にはget "show", to: PhotosControllerという書き方もできたらしいのですがこれからはget "show", to: PhotosController, action: "show"と書く必要があるとのこと
    • controller: /photos|users/という書き方が過去にできたみたいだけどこれも無効
    • routingをロードする前やRackアプリケーションをマウントしている時にrequest.controller_classを呼ぶとActionDispatch::MissingControllerを返すようになる

paramsをパターンマッチできるようになる

if params in { search:, page: }
  Model.search(search).limit(page)
else
  …
end

case (value = params[:primitive_or_nested])
in String
  # do something with a String `value`…
in { nested_key: }
  # do something with `nested_key` or `value`
else
  # …
end

Accept: text/markdownでリクエストを投げたときのエラーページをテキスト形式にする

CSRFトークンを使わずにCSRFを防げるようになる

rails routes --expandedでアクションメソッドの位置も見れるようになる

--[ Route 1 ]----------------------------------------------------------------------------------------------------------------------------
Prefix            | users
Verb              | GET
URI               | /users(.:format)
Controller#Action | users#index
Source Location   | /Users/shinichi.maeshima/tmp/2025/12/17/mainhoge/config/routes.rb:2
Action Location   | /Users/shinichi.maeshima/tmp/2025/12/17/mainhoge/app/controllers/users_controller.rb:5
--[ Route 2 ]----------------------------------------------------------------------------------------------------------------------------

さらにEDITOR環境変数が設定されているときにルーティングエラーページを表示すると次のようにペンのアイコンが表示されて、クリックするとエディタで対応するページが開くようになる。

ActionController::Liveで一部のコンテキストをコピーしないようにできる

class MyController < ActionController::Base
  include ActionController::Live

  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello world\n"
      sleep 1
    }
  ensure
    response.stream.close
  end
end
  • 実際にストリーミング送信は新規にスレッドを生成して実行している
    • このとき親となるスレッドからコンテキストをコピーして実行する、という仕組み
    • 状況によっては全部親のをコピーすると都合が悪い
    • 例えば複数DBでレプリカを使う、というコンテキストでストリーミングするけどストリーミング処理中ではDB書き込みをしたいというケース
    • config.action_controller.live.streaming_excluded_keys = [:active_record_connected_to_stack]のようにするとコピーしないように修正できる

docker環境でも開発時エラー画面からエディタを開けるようになる

{
  "containerEnv": {
    "EDITOR": "code",
    "RAILS_HOST_APP_PATH": "${localWorkspaceFolder}"
  }
}

401レスポンス時のconent-typeを変更できるようになった

http_basic_authenticate_with(
  name: "admin", password: "secret",
  message: '{"error":"Access denied"}',
  content_type: "application/json"
)

リクエストが安全なメソッドを利用しているかどうかを表すメソッドが追加された

ActionController::Parameters#fetch_valuesが追加された

params = ActionController::Parameters.new(name: "Francesco", age: 22)
params.fetch_values(:name, :age)                # => ["Francesco", 22]
params.fetch_values(:name, :none)               # => ActionController::ParameterMissing: param is missing or the value is empty or invalid: none
params.fetch_values(:name, :none) { |key| key } # => ["Francesco", :none]

ActionController::Parameters#deep_transform_valuesが追加された

ユーザごとにrate limitをかける、がやりやすくなった

class CommentsController < ActionController::API
  # Cache key in the store would be `rate-limit:comments:user/1`
  rate_limit to: 2, within: 2.seconds, by: -> { current_user }

  private

  # Substitute however you load your user object (potentially using Devise)
  def current_user
    User.find(1)
  end
end

Acceptヘッダに*/*が含まれていた時に必ずHTMLを返す、をやめた

トークン認証時に認証スキーム情報を渡せるようになった

  • Pass scheme to token authentication block in http_atthentication by chad-cole · Pull Request #58211 · rails/rails
  • トークンベースで認証させるとき、Authorization ヘッダーを利用することが多い
  • Railsではauthenticate_or_request_with_http_tokenなどを利用する
  • 渡されてきたトークンを検証するときに、トークンがなんのスキームで渡されてきたかという情報がなかった
  • DPoPというOAuth2.0の拡張仕様を利用するときはスキームの検証が必要
    • DPoPはクライアントの秘密鍵の署名をつけて送る手法で、トークンが盗まれても(秘密鍵がバレない限り)悪用がしづらい手法
  • というわけでauthenticate_or_request_with_http_tokenなどのブロック引数にschemeが追加された(optional)
  • authenticate_or_request_with_http_token(scheme: :dpop) do ...のようにすると受け付けるschemeをdpopに限定できる
  • 何も指定しないとtoken, bearer, dpopに限定される

QUERYメソッド対応が入った

Active Model

Errorsオブジェクトに引数に渡したエラー種別に関するメッセージを返すメソッドが追加された

person.errors.where(:name, :invalid).map(&:message)
person.errors.where(:name, :invalid).map(&:full_message)

もともと↑のように書けていたのが↓のようにかけるようになる

person.errors.messages_for(:name, :invalid)
person.errors.full_messages_for(:name, :invalid)

allow_nilやallow_blankに条件をつけることが簡単にできるようになった

validates(
  :course_id,
  inclusion: {
    in: -> (form) { form.permitted_courses.map(&:id) }
  },
  allow_nil: :saving_progress?
)
  • ifやunlessで同様のことはできるが、こっちのほうが簡潔にかけるぞという主張
  • バリデーション条件が複雑になるのは微妙だけど、選択肢が増えること自体はいいんじゃないでしょうか

has_secure_passwordでbcrypt以外にargon2も使えるようになった

json型のカラムのスキーマを定義できるようになった

class Account < ApplicationRecord
  has_json :settings, restrict_creation_to_admins: true, max_invites: 10, greeting: "Hello!"
  has_delegated_json :flags, beta: false, staff: :boolean
end

a = Account.new
a.settings.restrict_creation_to_admins? # => true
a.settings.max_invites = "100" # => Set to integer 100
a.settings = { "restrict_creation_to_admins" => "false", "max_invites" => "500", "greeting" => "goodbye" }
a.settings.greeting # => "goodbye"
a.staff # => nil
a.staff = true
a.staff? # => true
  • 次のようにしてjson型のカラムのスキーマを定義することができる
    • シンボルでプロパティの型を定義する(boolean, integer, string)か、デフォルト値を決めることができる
      • 決めている型以外の値がアサインされたらArgumentError
    • ネストは不可
  • デフォルト値はインスタンスを作るときとbefore_saveのタイミングで設定される
  • has_delegated_jsonは名前からわかるように各値へのアクセスを簡単にするためのメソッド
  • べんりな気もするけど、スキーマ決まってるならjsonじゃなくて普通にカラムを定義したらいいんじゃん?という気もしますね
    • サンプルコードのように設定値やフラグのようなものを一つずつカラムを定義して管理するのが面倒というケースを想定しているのかな

validatesのトップレベルの条件とvalidatorレベルの条件が両方とも効くようになった

validates :title, presence: { if: :local? }, if: :global?
  • 理由はHash#mergeでオプションのマージをしていたので、上の例だとifキーで衝突してglobalが勝つ
  • これを両方の条件を実行してANDで評価するような修正が入った
  • これはバグフィックスで良いと思うけどこのバグの振る舞いに依存しているコードとかあったりしないかやや心配

validates_numericality_of ... in: にprocやシンボルを渡せるようになった

validates_numericality_of :price, in: ->(o) { 0..o.max_price }

validates_numericality_of :price, in: :price_range

def price_range
  0..max_price
end

Active Record

enumのnegative scopeにnullなレコードも含むようになった

class Book < ApplicationRecord
  enum :status, [:proposed, :written, :published]
end
  • not_をつけるとnegative scopeとして、通常のscopeの否定のscopeができていた
    • not_proposed
    • not_written
    • not_published
  • enumが指定するカラムがNULLを含むときに、negative scopeでNULLを含むレコードが対象にならないのはおかしい、ということでNULLのカラムも対象になるようにnegative scopeが修正された
  • 非互換な変更なので設定で切り替えるのかな?と思いきやアップグレードガイドに変わるぞ、と追記されて完了
  • これ設定で切り替えるほうがいいんじゃないかな

explainのフォーマットを指定できるようになった

マイグレーションの仕方をDBごとにカスタマイズすることができるようになった

  • Add support for configuring migration strategy on a per-adapter basis. by adrianna-chang-shopify · Pull Request #56204 · rails/rails
  • マイグレーションのやりかたを設定する箇所(migration strategy)があり、これを利用するとカスタマイズしたオレオレマイグレーションをつくることができる
  • しかしこれはグローバルな設定であり、複数DB(例えばMySQLとPostgreSQLを併用する)のときにDBごとに別のmigration strategyを設定したいときに困る
    • そこでDBアダプタごとにmigration strategyを設定できるようにしたのがこのPR
  • DBごとにカスタマイズする、というユースケースはそれほどなさそうだけど、全体のmigrationのstrategyを変更できるとこういう便利さがあるぞという例を紹介してくれているブログがあるので参考までにリンクを載せておきます

with_lockのブロック引数にTransactionオブジェクトが渡されるようになった

person.with_lock do |transaction|
  transaction.after_commit { puts "hello" }
end

query_log_tagsに追加できるコンテキストにsqlが追加された

  • Pass sql query to query log tags by fatkodima · Pull Request #56683 · rails/rails
  • 前提
  • 今回、query_log_tagsの情報としてsqlそのものを追加できるようになった
    • そのまま使うとセンシティブな情報がそのまま出る(しそもそもクエリのコメントとして出力されるので無駄な)ので、例のようにクエリの長さとか、何か加工したものを出力する想定
    • どういうのがコメントとして出力されていると便利なんでしょうねえ
      • PR中ではIN句に指定されたものの個数とか書いてある

トランザクションにフックして何かする、が簡単にできるようになった

class Account < ApplicationRecord
  private
    def implicit_persistence_transaction(connection, &block)
      if connection.transaction_open?
        yield # avoid opening a transaction if one is already open
      else
        super
      end
    end
end
  • この例だとトランザクションをすでに開いていたらなにもせずに実行するというようなコードになっている
  • Shopifyではこれを使って「特定のモデルであれば決められた分離レベルで実行する」がやりたいみたい

create_table時に発行されるクエリをまとめて実行するようになった

add_indexに配列を渡せるようになった

t.index 'lower(email), status'
  • これが次のように配列で渡すことができるようになる
t.index [ "lower(email)", :status ]
t.index [ "lower(email)" ]
  • ちょっとした変更だけど、式じゃなければ元々配列でかけたので一貫性としてはいいんじゃないでしょうか

in_order_ofの引数をグループ化できるようになった

Posts.in_order_of(:state, [[:published, :canceled], :archived]).order(created_at: :desc)
# =>
# SELECT "posts".* FROM "posts" WHERE "posts"."state" IN (1, 2, 3)
# ORDER BY CASE
#   WHEN "posts"."state" IN (1, 2) THEN 1
#   WHEN "posts"."state" = 3 THEN 2
#  END ASC, "posts"."created_at" DESC

insert_allとupsert_allでキーが揃っていない引数を渡した時のエラーメッセージが改善された

Book.insert_all [
  { name: "Rework", author_id: 1 },
  { name: "Remote", author_id: 1, isbn: "..." }
]
# Before:
# ArgumentError: All objects being inserted must have the same keys
# 
# After:
# ArgumentError: All objects being inserted must have the same keys (extra: ["isbn"])
  • このAI時代、エラーメッセージが改善されるとトークンが節約できるので、エラーメッセージが今までよりも大事になっている気がします

enumでinvalidな値を指定したときのエラーメッセージの内容がより詳細になった

Book.new(status: "bogus") 
# Before:
# ArgumentError: 'bogus' is not a valid status
#
# After:
# ArgumentError: 'bogus' is not a valid status. Valid values are: "proposed", "written", "published"

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

class Post < ApplicationRecord
  has_many :comments, default_order: :likes
  # .. or ..
  has_many :comments, -> { default_order(:likes) }
end

post = Post.first
post.comments # comments ordered by `likes`
post.comments.order(:created_at) # comments ordered by `created_at`

query log tagがコネクションプールごとに設定できるようになった

db/schema.rb にこれまで実行したmigrationのリストを追加できるようになった

ActiveRecord::Schema[8.2].define(version: 2026_04_16_011000) do
  # ...
end
  • これは次のようなエッジケースで困る
    • 自分のブランチでまだ実行していないmigrationファイルが存在する状態でmainブランチをmergeする
      • versionが、自分のmigrationファイルよりも新しくなる
    • bin/rails db:resetする
    • config/schema.rbが適用される
    • このとき、versionより古いマイグレーションは全て適用済み扱いになる(!!)
    • よって、この後bin/rails db:migrateしても自分のブランチでまだ実行していないmigrationファイルは実行されない
  • エッジケースの方はそれほど踏んだ人は多くなさそうだけど、versionのところでコンフリクトするのはよくある
  • これらの問題を解決するために、実行したmigrationのバージョンを全部schema.rbに書くようにする、というのがこのPR
  • config.active_record.dump_schema_migrations = trueとしたときに有効になる(デフォルトはfalse)
  • 次のように、schema.rbの末尾に適用済みマイグレーションのバージョンが追記される
ActiveRecord::Schema[8.2].define do
  # ...
end

ActiveRecord::Schema.load_schema_migrations(__FILE__)
__END__
20260723024750
20260723024804
config.active_record.dump_schema_migrations_sort_by = ->(version) { Digest::MD5.hexdigest(version) }

query log tagsとprepared statementsを同時に使えるようになった

スキーマキャッシュから特定のテーブルを除外する設定が統合された

  • Add config.active_record.schema_ignored_tables by edcarvio · Pull Request #58554 · rails/rails
  • スキーマキャッシュは、Railsが起動するときにスキーマ情報を取得する際のクエリを投げずに済ませるために使えるキャッシュ
    • スキーマキャッシュなしでたくさんのRailsアプリケーションプロセスが一斉に起動すると、DBに無駄に負荷がかかる
  • 特定のテーブルをスキーマキャッシュから除外するときに、これまで二つの設定があった
    • ActiveRecord::SchemaDumper.ignore_tables: DBスキーマの出力(schema.rb)から除外する設定
    • config.active_record.schema_cache_ignored_tables: スキーマキャッシュの生成から除外する設定
  • 二つに分かれてる意味ないやろ、ということで config.active_record.schema_ignored_tablesに統合するぞ、というのがこのPR
  • 従来の二つの設定はdeprecatedになっています

「SELECTでorderがない場合はshuffleして返す」ができるようになった

  • activerecord: add an option to shuffle unordered SELECT results by viralpraxis · Pull Request #58548 · rails/rails
  • SELECTでorderをつけない時の順序は不定だけど大体同じ順番で返ってきてしまうので、テストでは気付けないことが多い
  • これに気づきやすくするための設定が追加された
  • ↓の設定を書くと、orderなしのクエリでDBから複数の結果が返ってきた場合、必ずshuffleをするようになる
    • DBでshuffleしているわけではなくて、あくまでArray#shuffle!を利用している
    • なのでfirstやlastなどのメソッドで一つのレコードだけ取得する場合は対象外
    • arelでorderがあるかないか判別しているので、find_by_sqlなどでSQL文字列を直接渡しているような場合は対象外
    • orderの指定の仕方によっては不定にケースがあるけどorderがついていれば対象外
config.active_record.shuffle_unordered_selects = true
  • 100%気付けるわけではないにしろ、テスト環境でオンにしておくと便利じゃないかと思います

PostgreSQLのエラーメッセージの詳細度をdatabase.ymlから指定できるようになった

ERROR: duplicate key value violates unique constraint "index_users_on_email" DETAIL: Key (email)=(alice@example.com) already exists.
  • 個人情報をログやSentryに出したくない!!!
  • PostgreSQLにはメッセージの詳細度を変更するための関数PQsetErrorVerbosityがある
  • これをdatabase.yml経由で設定できるようになった
    • ↓のようにすると、エラーメッセージにメールアドレスが含まれなくなる
production:
  adapter: postgresql
  error_verbosity: <%= PG::PQERRORS_TERSE %>
  • べんり

Action Mailer

ActionMailerのassertionの追加

mail = UsersMailer.create(user)
assert_part :text, mail do |text|
  assert_includes text, "Welcome, #{user.email}"
end
assert_part :html, mail do |html|
  assert_dom html.root, "h1", text: "Welcome, #{user.email}"
end

assert_no_part :html
assert_no_part :text

Action View

collectionをrenderするときにブロックを渡せるようになった

# _post.html.erb
<article class="post">
  <h1><%= post.title %></h1>
  <div class="post__body"><%= post.body %></div>

  <%# Render additional content %>
  <%= yield post if block_given? %>
</article>

# index.html.erb
<%= render @posts do |post| %>
  <%= link_to "Edit", [:edit, post] %>
<% end %>

ビューヘルパで属性をネストした時にハイフンでつなぐようになる

before

tag.button "POST to /clicked", hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }
=> "<button hx=\"post /clicked swap outerHTML data {json: true}\">POST to /clicked</button>"

after

tag.button "POST to /clicked", hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }
# => <button hx-post="/clicked" hx-swap="outerHTML" hx-data="{&quot;json&quot;:true}">POST to /clicked</button>

number_fileld_tagとrange_field_tagにendless、beginlessのrangeを渡せるようになる

color_fieldにvalue: nilを渡すとnilが無視されるのが無視されなくなった

ERBの設定をする口が公式にできた

  • Allow to configure ERB options through ActionView::Base by Edouard-chin · Pull Request #58359 · rails/rails
  • 元々コミッタとしてはprivate APIのつもりだったのだけれど、publicなAPIとしてドキュメントに載っていた時期があり、これに依存しているアプリケーションもあるとのことでpublicな口を作った、という流れらしい
  • 元々publicだったerb_trim_mode=に加えて三つの設定が追加されている
    • erb_implementation=
    • escape_ignore_list=
    • strip_trailing_newlines=

Rails8.2ではherbがデフォルトのテンプレートエンジンになる

Active Storage

Active Storageでサムネイル変換を即時で行えるような選択肢が追加された

  • ActiveStorage immediate variants by tomrossi7 · Pull Request #51951 · rails/rails
  • これまでActiveStorageのサムネイル作成は、サムネイルが呼ばれたタイミングで作るもしくはActiveJob経由でバックグラウンドで作成することができた
    • 作ってすぐ大量にサムネイルへのアクセスがあるときに困る
    • 同時に大量の同一サムネイル生成が必要になって競合する
  • そこで新しいオプションが追加されて、次のようにimmidiate: trueをオプションとして付与することで、同期的にサムネイルを作ることができるようになった
  • 実装はjobで作られていてperform_nowとperform_laterを切り替えるだけにしている
  has_one_attached :avatar_with_immediate do |attachable|
    attachable.variant :thumb, resize_to_limit: [4, 4], immediate: true
  end

ActiveStorageで即時サムネイル生成するときに解析もやるようになった

  • Active Storage: eager analysis and local file processing for immediate variants by jeremy · Pull Request #56327 · rails/rails
  • もともとActiveStorageでアップロードしたファイルはActiveStorage::AnalyzeJobで非同期に解析処理が行われていた
    • AnalyzeJobでは例えば画像ならwidthとheightをメタデータとしてDBに保存する
  • 即時サムネイル生成を有効にしたときに解析処理も即時で行うようになった
  • ついでにafter_uploadができるようになった
    • これまではafter_create_commitが使われていたが、すでに存在するblobについてはcommitしないのでうまく機能しなかったっぽい
    • 恐らくblobは同じでもattachmentが違って、それぞれ別々のサムネイルが作られる、というユースケースを考えているのかな(PRのコメントが簡潔なのでこのあたり推測)
ActiveStorage::Attachment.after_upload do
  # Your custom logic here
end

アップロードファイルの解析は遅延処理ではなくバリデーション前に行うのがデフォルトになる

Active StorageがAPIモードで使いやすくなった

ActiveStorageにattach!が追加された

  • ActiveStorage: implement attach! by quentindemetz · Pull Request #54416 · rails/rails
  • attachメソッドは対象モデルがDBに永続化されていて、変更されていなければ引数のファイルを即時saveするメソッド
  • attach!はsaveとsave!の関係と同じで、バリデーションに失敗するなどで保存に失敗をしたら例外(ActiveRecord::RecordNotSaved)を発生させる
  • トランザクション中でattachさせて、失敗したらロールバックしたいぞ、みたいな時には便利かと

has_many_attachedで関連づけられているファイルの容量合計を返すヘルパーメソッドbyte_sizeが追加された

document.images.byte_size # => 2048

ImageProcessing gemのメジャーバージョンアップ

variant生成に独自のクラスを指定できるようになった

ffmpegとffprobeに対してオプションを渡せるようになった

marcelのバージョンが2にアップグレードされた

ActiveStorageのダイレクトアップロードに関するルーティングを設定しない、ができるようになった

Active Job

一回deprecatedになったconfig.active_job.enqueue_after_transaction_commitの設定が復活した

retry_onのwaitに渡すprocの第二引数にエラーオブジェクトが追加された

class RemoteServiceJob < ActiveJob::Base
  retry_on CustomError, wait: ->(executions, error) { error.retry_after || executions * 2 }

  def perform
    # ...
  end
end

Active Jobにattributeメソッドが追加されて、Active Job Continuation利用時に途中経過を保存するのがやりやすくなった

  • Add ActiveJob::Attributes to persist data between steps by bdewater-thatch · Pull Request #57208 · rails/rails
  • 次のように、ActiveModelのattributeをActiveJobでも使えるようになった
  • これはActiveJob::Continuationの機能の一部
    • デプロイ時など、途中でジョブを終了する時にどこまで終了したかをジョブの情報として残す
    • さらにattributeとして指定した属性も一緒にジョブの情報として残すことができる
    • これまではActiveRecordなどを利用して途中経過を保存する必要があったけど、そこまででもないものはここに保存すると良さそう
    • include ActiveJob::Continuableとせずにinclude ActiveJob::Attributesとしてattributeだけを使うこともできる
      • でもattributeだけ使えてもあんまり有効活用はできなさそう
class SubmitEnrollmentJob < ApplicationJob
  include ActiveJob::Continuable
  
  attribute :payment_token, :string
  attribute :billing_profile_id, :integer
  
  def perform(enrollment)
    step(:tokenize_payment_instrument) do
      self.payment_token = PaymentGateway.tokenize(enrollment.user.payment_instrument)
    end
    
    step(:create_billing_profile) do
      self.billing_profile_id = BillingProfileApi.create(customer_id: enrollment.user_id)
    end
    
    step(:submit_enrollment) do
      submission_id = EnrollmentApi.submit(enrollment, billing_profile_id)
      enrollment.update!(status: 'processing', submission_id: submission_id)
    end
  end
end

特定のキューを中断する時にActive Job Continuationの中断ができるようになる

retry_onのwaitオプションにfloatを渡せるようになった

DBの接続が不調でエラーな時はリトライしたい

  • Introduce ActiveJob::DeserializationError::RecordNotFound by rosa · Pull Request #57742 · rails/rails
  • HeyでDB接続が不調で、引数のデシリアライズ中にエラーになりジョブの実行が失敗する、ということがあった
    • この時のエラーはActiveJob::DeserializationErrorという広い意味でのエラーになる
    • discard_on ActiveJob::DeserializationErrorとしているとリトライができない
  • DBへのコミットからジョブ実行までに該当レコードが削除されているような時はdiscard_onで良いのだけど、DB接続が不調なときはリトライさせたい
  • というわけでActiveJob::DeserializationErrorのサブクラスとして ActiveJob::DeserializationError::RecordNotFoundが新設された
  • discard_on ActiveJob::DeserializationError::RecordNotFoundとすれば「DBへのコミットからジョブ実行までに該当レコードが削除されている」時だけdiscardできる

Action Text

Action Textにto_markdownが追加された

Railsの出力する標準のログ出力がStructured Event経由に変更になった

Credentialsと環境変数をまとめて取り扱う仕組みができた

Rails.app.creds.require(:db_host) # ENV["DB_HOST"] || Rails.app.credentials.require(:db_host) 
Rails.app.creds.require(:dataase, :host) # ENV["DATABASE__HOST"] || Rails.app.credentials.require(:database, :host)
Rails.app.creds.option(:cache_host, default: "cache-host-1") # ENV["CACHE_HOST"] || Rails.app.credentials.option(:cache_host)
Rails.app.creds.option(:cache_host, default: -> { "cache-host-1" }) # ENV["CACHE_HOST"] || Rails.app.credentials.option(:cache_host) || "cache-host-1"
  • 環境変数だけを対象にしたenvsメソッドも同じように扱える
Rails.app.envs.require(:db_password) # ENV,fetch("DB_PASSWORD")
Rails.app.envs.require(:aws, :access_key_id) # ENV.fetch("AWS__ACCESS_KEY_ID")
Rails.app.envs.option(:cache_host) # ENV["CACHE_HOST"]
Rails.app.envs.option(:cache_host, default: "cache-host-1") # ENV.fetch("CACHE_HOST", "cache-host-1")
Rails.app.envs.option(:cache_host, default: -> { HostProvider.cache }) # ENV.fetch("CACHE_HOST") { HostProvider.cache }
  • 開発環境の時だけ.envも読む
    • デフォルト設定だと.env決め打ちで、.env.developmentみたいなファイルは考慮していない
  • 環境変数と.env、Credentials以外に対象となる要素を追加することもできるので、config/application.yml なども追加したら全てRails.app.credsで賄うこともできそう
    • そのときはcredsからリネームしたほうが良いかもしれない

Railsのお作法に則っていないgemを検出する機能がRails公式に入る

#{component.inspect} was loaded before appliction initialization.
Prematurely executing load hooks will slow down your boot time
and could cause conflicts with the load order of your application.
Please wrap your code with an on_load hook:

  ActiveSupport.on_load(#{component.inspect}) do
    # your code here
  end

次のように設定するとログ出力の代わりに例外にすることができる

config.action_on_eary_load_hook = :raise

tメソッドのscopeを現在のアクション/テンプレートの相対パスで表現できるようになった

# always works
t(".foo.bar") # => posts.index.foo.bar

# before this PR
t("bar", scope: ".foo") #=> foo.bar 

# after this PR
t("bar", scope: ".foo") #=> posts.index.foo.bar 

ginza.rb 第98回を開催してspinelについて学んだ

Ginza.rb 第98回 - spinelについて学ぶぞ - connpass

第98回はmatzの新作である matz/spinel を題材にみんなで学びを深めました。

今回が最後のメドピア株式会社さんの会場提供でした。過去のイベントの情報を見たところ、第57回 からお世話になっていたようです。長年のサポートありがとうございました!

y-yagiさんが資料をまとめてくれた のでこれを見ながらワイワイしました。

感想

  • spinelは最初「Rubyランタイムのない環境でもRubyを配布して動かす」系のプロダクト(例: Tebako )なのかな、と想像していたのですが少なくとも現時点ではそうではないというのがわかりました。Windows対応がWSL実行のみサポートとのこと
    • 「いろんな環境にバイナリを配る」という用途だとgolangが選ばれがちだけど、これをspinelが置き換えられたらいいなあと思っている
  • ThreadがM:Nモデルなのがパフォーマンス的に有意に効きそうだなあという気持ちになりました
  • Cライブラリを使うのがCRubyよりも楽そう
  • 基本的に黒魔術系は禁止なのでRailsアプリケーションを普通に動かすのは難しそう
    • 普通じゃないやり方(Railsアプリケーションをトランスパイルする)でRailsアプリケーションを動かそうとしている人、Sam Rubyさんがすごいねという話をした
    • rubys/roundhouse: Rails as a specification; the deployment target is a build flag
    • 今年の終わりまでにMastdonをspinelで動かすぞ、という目標で頑張っているらしい
    • spinelで仕事のRailsアプリケーションを動かすことができると、明確にスループットとメモリ使用量が改善すると思うので夢がありますね
  • 今も活発に改善が行われているので、この認識も少し時間が経過したら変わっているのかもしれません

次回

10月7日(水)に、株式会社マイベストさんの会場をお借りして開催します。お題はRails8.2です。ご都合の合う方はどうぞ。 Ginza.rb 第99回 - Rails8.2について学ぶぞ - connpass

ginza.rb 第97回を開催してRubyKaigi 2026を振り返った

Ginza.rb 第97回 - RubyKaigi 2026を振り返る - connpass

第97回は函館で開催されたRubyKaigi 2026の内容についてみんなでワイワイ振り返りました。

進め方

参加者の中に登壇者がお二人(dak2さんとkoicさん)がいたので2人の発表内容や登壇した感想などを話してもらい、そこからスケジュールを眺めて気になった発表について話したい人が話していく、という流れで進めました。

特にタイムキープ等せずに進めたらDay 2まででほぼ時間切れ><1時間ちょいで全部を振り返るには内容が多すぎましたね…。

個人的に気になった発表

個人的には、差分に対応する必要なテストを選別するための方法の発表が一番印象に残りました。

rubykaigi.org

僕の見ている範囲では、いろんな会社がCIサービスに対しての支払いを何とか減らそうと色々工夫しています。

工夫の内容としては、例えばデフォルトではCIを回さずに明示的にラベルなどをつけた時にだけCIを回す運用とか。そうするとCIを回したつもりが忘れていて困るみたいなことが時折起きます。コストは下げられるけど開発者体験とのトレードオフになっている感じがあります。

あとは例えばビューだけを変更したPRであればモデルの単体テストはスキップしても大丈夫なので、明らかにスキップできるアプリケーション差分とテストの対応表を頑張って作って適用するとか。これは便利だけど、作りこんで維持するのは大変そうだなという印象。

発表で触れられていたDatadogのTest Optimizationサービスを使っていればこの問題を良い感じに解決できるようですが、Datadogユーザでなくてもお手軽になんとかしてくれる仕組みを作れないもんかな〜と思っています。時間ができたらやりたい。

あとは僕は発表は見れてないのですがrubydex、ASTro、tutorialkit.rbあたりは後で復習しておかないとな…となりました。複数トラック形式だと物理的に全ての発表を聞くことはできない*1ので、他の参加者の視点を教えてもらえるのはとても助かります。

コミュニティかわらばん用の寄せ書きを募集しています

「コミュニティかわらばん」であなたの活動を届けませんか?|関西Ruby会議に乗っかりたいな、と思っているのでginza.rbメンバーからの寄せ書きを募集しています!こちらからお気軽にどうぞ。↓のポストにも書いているけど、一回でもミートアップに参加していればginza.rbメンバーです!

コミュニティかわらばんの寄せ書きを集めるスレ · Issue #1301 · ginzarb/meetups

次回

次回は7月24日(金)にspinelをお題にしてワイワイする予定です。ご都合の合う方お気軽にご参加ください (( ⁰⊖⁰)/)

Ginza.rb 第98回 - spinelについて学ぶぞ - connpass

*1:あと体力的にずっと発表を聞き続けるのも厳しい><

lambyを利用したRackアプリケーションが起動しなくなったのを直した

RackアプリケーションをLambdaでホスティングしてみたの続編です。

lamby を使ったRackアプリケーションはここ1年ほど特に問題なく運用できていたのですが、先日Lambdaで動かしているアプリケーションのヘルスチェックが失敗するようになりました。CloudWatch Logsを見てみると次のようなエラーが出ていました。

/usr/local/lib/ruby/3.2.0/bundler/runtime.rb:304:in `check_for_activated_spec!': You have already activated logger 1.5.3, but your Gemfile requires logger 1.7.0. Since logger is a default gem, you can either remove your dependency on it or try updating to a newer version of bundler that supports logger as a default gem. (Gem::LoadError)

lambyでは次のように、aws_lambda_ricというAWS公式のRuby用のクライアントを経由してRackアプリケーションを実行しています。これはlambyの公式ドキュメントにあるコードをそのまま引用して利用しています。

FROM ruby:3.2-bullseye

RUN gem install 'aws_lambda_ric'
ENTRYPOINT [ "/usr/local/bundle/bin/aws_lambda_ric" ]
CMD ["config/environment.Lamby.cmd"]

僕の作ったRackアプリケーションはGemfileで管理されたsinatraを利用していました。上記のコードを見てわかるように、aws_lambda_ricはGemfile管理外で実行されています。そしてaws_lambda_ricとsinatraはどちらもloggerを依存関係として持っています。

ログメッセージとこれらの事実を繋ぎ合わせるとaws_lambda_ricとsinatraのロードするloggerのバージョンが違うのでエラーになっていたということがわかります。Gemfileにaws_lambda_ricを明示的に追加して、ENTRYPOINTをbundle exec形式に変更することで解決しました。

FROM ruby:3.2-bullseye

RUN gem install 'aws_lambda_ric'
ENTRYPOINT [ "bundle", "exec", "aws_lambda_ric" ]
CMD ["config/environment.Lamby.cmd"]

これは本家にフィードバックしようと思ったのですが、gemそのものの問題ではないのでIssueを作りづらいな…となりとりあえずブログとして供養しておこうと思ってエントリを認めた次第です。

ginza.rb 第96回を開催してFizzyについて学んだ

Ginza.rb 第96回 - Fizzyソースコードリーディング - connpass

第96回は37signals社が公開したFizzyのソースコードを見ながらワイワイしました。

basecamp/fizzy: Kanban as it should be. Not as it has been.

fizzyというビジネスに対する感想

第96回を開催したタイミングでは「1000カード以上を作ろうとすると20ドル/月」という課金体系でした。ユーザ数などは無制限でだいぶ安い。さらにコードが公開されているのでオンプレでも動かせる。オンプレからクラウド、クラウドからオンプレへの移行もやりやすいようにuuid7で作っていて(それぞれが稼働している状態でデータをマージしてもidが衝突しない)、ユーザとしてみたら最高便利なんだけどサービス提供側からしたらこれで採算取れるのかな?と思っていたら完全無料のサービスになってしまって驚きました。元々どういうビジネスプランだったのか気になるところです。

設計や実装に対する感想

色々気になった点を列挙しています。

  • 大まかなコード体系としてはbasecamp/once-campfireと似ています(同一の会社のプロダクトなので当たり前だけど)。最近のプロダクトなのでAGENTS.mdだったりSTYLE.md)だったりとAIエージェントに読ませるためのファイルがあるのが違い。
  • 大きな特徴としてはsqlite3とmysqlのどちらでも使える構成だということ。元々はsqlite3で実装する予定だったけど途中で厳しいことがわかりSaaS版はmysqlにしたというのをどこかで読みました。
  • 全文検索をDBで実装しているというのも珍しいところです。一昨年のRails WorldでDHHが話していたActive Record Searchの原型はこれなんだろうと思っています。しかし実装としてはMySQLとsqliteの全文検索機能を比較的素朴に使っており、単語はスペースで区切られるのが前提になっているため日本語でFizzyを使うと検索はまともに動きません。残念><
  • seedの定義の仕方も面白いです。まずseed用のDSLを定義して、各テナントでDSLを利用してレコードを作っています。
  • マルチテナントなSaaSとしてテナントを分けるのをRackミドルウェアで実装している、というのも個人的には面白い点だと思いました
  • あとはライセンスが興味深かったです。基本MITと同等だけど競合サービスを作るのは禁止というO’Saasyライセンスを新設しています。
  • koicさんも書いてたけどzip_kitを知れたのは収穫でした。

次回

次回は5月15日(金)に、RubyKaigiの振り返りをやります。興味のある方はぜひご参加ください。

Ginza.rb 第97回 - RubyKaigi 2026を振り返る - connpass

seed-doのv4.0.0をリリースしました

seed-do v3.2.0をリリースしました - おもしろwebサービス開発日記 で書いていた速度改善案を実装してv4.0.0としてリリースしました。

rubygems.org

SeedDo.seed(bulk: true)とするとRailsのupsert_allを使用して一括でレコードをUPSERTできるようにしました。

PRはこちら

github.com

大半の人は、単にSeedDo.seedをSeedDo.seed(bulk: true)に置換するだけでseedの実行が速くなるんじゃないかな、と期待しています。ただし upsert_allの:unique_byオプションを利用している関係でPostgreSQLとSQLiteでしか利用できません。MySQLユーザの人はごめんなさい。

また、この機能を入れるにあたって内部APIの整理をした関係で非互換が発生したのでメジャーバージョンアップをしています。内部APIを使ってなければ関係ないはず。

利用してみて、何か感想などあれば教えてもらえると嬉しいです。

株式会社ウィルネットは設立9周年を迎えました

2月21日は弊社の設立記念日でした。もう会社運営も10年目。時間が過ぎるのはあっという間ですね…。

去年のエントリはこちら:

blog.willnet.in

法人9年目を振り返る

2025年の弊社(僕)の活動を振り返ると、OSS活動をよくやっていた印象があります。仕事の関係で見ていたライブラリのメンテが滞っていて仕方ないから直すか〜というのを繰り返していたらいつの間にかメンテナになっていたりしました。AI AgentのおかげでコミットメッセージやPRの英語に困らなくなったというのが要因として大きい気がします。

メンテされないgemの向き合い方についてfindyさんに寄稿したりもしました。

findy-code.io

あとはブログのエントリ数が2024年の倍以上あるのでまあまあアウトプットを頑張っていそう。お仕事の一環で僕が新しく知ったり学んだりした情報を共有するというのを継続してやっていて、そこでのアウトプットで満足してパブリックな状態にはせずにいたのですがやっぱりパブリックにもした方がいいよね、ということで時間や気力的に可能な分だけ公にしています。特にRails8.1のマイナーフィーチャーをまとめました みたいなやつがパブリックになっていると自分でも便利なのでもっとやっていきたい(時間があれば…)。

最近はAI Agentのおかげで時間がない中でもちょっとしたタスクを進めることができて楽しいです。仕事もプライベートもなんやかんやで忙しいのですが、顧問先に関しては週1日程度分を募集しています。興味ある方はお声がけください。

10年目も健康に気をつけつつ頑張っていきます (( ⁰⊖⁰)/)