🚀

Rails 7.2 → 8.0 アップグレードと Sprockets から Propshaft 移行

に公開

Rails 7.2.2.2 から 8.0.3 へのアップデートを行い、併せて sassc-rails から cssbundling-rails、Sprockets から Propshaft への移行を実施したため、その手順を記録しておきます。

参考:Rails アップグレードガイド – Railsガイド

Railsバージョンアップ

Gemfile
- gem 'rails', '~> 7.2.2.2'
+ gem 'rails', '~> 8.0.3'
$ bundle update

アップデートタスク

$ rails app:update

差分を確認して必要な設定を戻します。
またrailsdiff.orgを参考にして、新しく追加された設定を確認します。

Gemバージョンアップ

railsdiff.orgを参考にgemを更新。

Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.3.7'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
gem 'rails', '~> 8.0.3'

- # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
- gem 'sprockets-rails'
+ # The modern asset pipeline for Rails [https://github.com/rails/propshaft]
+ gem 'propshaft'
# Use postgresql as the database for Active Record
gem 'pg'
gem 'psych', '5.2.6'
# Use Puma as the app server
gem 'puma', '~> 6.5'
- # Use SCSS for stylesheets
- gem 'sassc-rails'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
# gem 'importmap-rails'
gem 'jsbundling-rails'
+ gem 'cssbundling-rails'

Sprockets を Propshaft に移行

Rails 7 まではデフォルトのアセットパイプラインが Sprockets だったが、Rails 8 からはPropshaft に置き換わる事になったので移行しました。

issue:Make Propshaft the default asset pipeline in Rails 8

参考:Rails: Sprockets->Propshaftアップグレードガイド(翻訳)

$ gem uninstall sprockets-rails

You have requested to uninstall the gem:
        sprockets-rails-3.5.2

sassc-rails-2.1.2 depends on sprockets-rails (>= 0)
If you remove this gem, these dependencies will not be met.
Continue with Uninstall? [yN]

sassc-rails という別の gem が sprockets-rails に依存しているため、gem uninstall sprockets-rails を実行するとアプリが動かなくなる可能性が高い という警告が出ました。
なのでまずは sassc-rails をやめて cssbundling-rails に移行する対応から行いました。

1. sassc-rails を cssbundling-rails に移行

    1. Gemfile から sassc-rails を削除後、cssbundling-rails を追加
    1. bundle install 実行後、rails css:install:sass を実行

手順が完了すると、いくつかのファイルが更新されていることがわかります。

    1. 新たに作成された application.sass.scss を削除後、package.json の css ビルド対象を変更

現状、新しく作成された application.sass.scss が cssbundling-rails 用のビルドエントリポイントとなっています。
名前は自由ですが慣例として application.scss に統一する事にしましたので application.sass.scss を削除しました。
それに伴って package.json の css ビルド対象も修正しました。

package.json
{
  "name": "app",
  "private": true,
  "scripts": {
    "lint": "npm-run-all lint:*",
    "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore ./app/javascript/",
    "lint-fix:js": "eslint --fix --ext \".js,.vue\" --ignore-path .gitignore ./app/javascript/",
    "lint:style": "stylelint 'app/assets/stylesheets/**/*.scss'",
    "lint:secret": "secretlint --secretlintignore .gitignore $(git ls-files)",
    "build": "webpack --config webpack.config.js",
-   "build:css": "sass ./app/assets/stylesheets/application.sass.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
+   "build:css": "sass ./app/assets/stylesheets/application.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  },
  "dependencies": {
    1. application.scssの修正

*= require_tree .は Sprockets 用の manifest コメントであり、Propshaft + cssbundling-rails に移行する場合、使えないので削除しました。

app/assets/stylesheets/application.scss
-/*
- * This is a manifest file that'll be compiled into application.css, which will include all the files
- * listed below.
- *
- * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
- * vendor/assets/stylesheets directory can be referenced here using a relative path.
- *
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
- * compiled file so the styles you add here take precedence over styles defined in any other CSS
- * files in this directory. Styles in this file should be added after the last require_* statement.
- * It is generally better to create a new file per style scope.
- *
- *= require_tree .
- *= require_self
- */

@charset "utf-8";

.font-weight-200 {
  font-weight: 200;
}

パーシャルを読み込む際は Sprockets の require_tree の代わりに @use / @import で明示的に読み込むようにします。(パーシャルファイルは 必ず _ から始める)

2. Sprockets を Propshaft に移行

    1. Gemfile から sprockets-rails を削除後、propshaft を追加
    1. bundle install 実行
    1. app/assets/config/manifest.js、app/assets/javascripts/application.js、config/initializers/assets.rb 削除

Sprockets 専用のこれらのファイルは不要になったので削除しました。

app/assets/config/manifest.js
-//= link_tree ../images
-//= link_tree ../../javascript .js
-//= link_tree ../builds
-//= link application.js
app/assets/javascripts/application.js
-// This is a manifest file that'll be compiled into application.js, which will include all the files
-// listed below.
-//
-// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
-// vendor/assets/javascripts directory can be referenced here using a relative path.
-//
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file. JavaScript code in this file should be added after the last require_* statement.
-//
-// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
-// about supported directives.
-//
-//= require rails-ujs
-//= require activestorage
-//= require_tree .
config/initializers/assets.rb
-# Be sure to restart your server when you modify this file.
-
-# Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = '1.0'
-
-# Add additional assets to the asset load path.
-# Rails.application.config.assets.paths << Emoji.images_path
-Rails.application.config.assets.paths << Rails.public_path.join('assets')

Rails 8.2 への移行で発生した問題とその解決策

1. 日付/時刻の厳格化

発生した問題

Rspecを実行すると下記エラーが出ました。

ActiveRecord::StatementInvalid:
    PG::DatetimeFieldOverflow: ERROR:  date/time field value out of range: "0"
    CONTEXT:  unnamed portal parameter $31 = '...

下記はサンプルコードですが、PostgreSQL で Rails 8 からは timestamp 型に 0'777'といった数値や文字列を代入する事が許可されなくなりました。
※ Rails 7 では暗黙的に NULL として保存されていました。

record = Record.new(
  id: 1,
  name: "サンプルデータ",
  status: :approved,
  updated_at: 0
)

解決策

updated_at: 0 を 削除 or nilに変更。

record = Record.new(
  id: 1,
  name: "サンプルデータ",
  status: :approved
)

参考:ActiveRecord::StatementInvalid: PG::DatetimeFieldOverflow: ERROR: date/time field value out of range #54110

2. Strong Parameters の expect で optional パラメータを扱えない問題

発生した問題

Rails 8 で params.expect を使って optional なパラメータを検証しようとしたところ、下記エラーが発生しました。

params.expect(order: [line_items_attributes: %i[price quantity]])
# => ActionController::ParameterMissing: param is missing or the value is empty or invalid: order

今回のリクエストでは line_items_attributes が存在しない場合もありました。そのため expect が「必ず存在するはず」という前提で検証した結果、例外が発生しました。

解決策

optional なパラメータには permit を使う。親キーのみ必須でチェックし、サブキーは存在する場合だけ許可するよう修正しました。

  • require(:order) で親キーの存在はチェック
  • line_items_attributes は存在する場合のみ取得
  • 存在しない場合は空ハッシュ {} が返るため、安全に処理可能
params.require(:order)
      .permit(line_items_attributes: %i[price quantity])

その他

Rails 8 へのアップデートとは直接関係は無いが、gem のバージョン未更新でハマった箇所があったのでメモ書き程度に書き残しておきます。

1. FactoryBotでAssociationをオーバーライドする際のバリデーション問題を解決した話

発生した問題

factory_bot 6.5.4 から 6.5.5にアップデート後、Rspecを実行すると下記エラーが発生しました。

元の Factory は以下のように書かれていました。

factory :item_price_online, parent: :item_price do
  sales_channel { create(:sales_channel, code: 'online') }
end

そして、テストでは「販売チャネルが未入力の場合」を確認しようとしました。

context '販売チャネルが未入力の場合' do
  let(:subject_price) { FactoryBot.build(:item_price_online, sales_channel_id: nil) }
end

しかし、テストを実行するとエラーが発生しました。

  • 元の Factory では sales_channel が常に作成される仕様
  • そのため、sales_channel_id: nilのオーバーライドが Factory 内で無効化される
  • Association のオーバーライドより Factory 内のブロックが優先される Rails / FactoryBot の挙動

結論、factory 内で Association が必ず作成されるため、オーバーライドが効かずテストできなかった。

参考:Fix association override precedence over trait foreign keys

解決策

Factory に transient 属性を追加して、テスト専用のフラグで制御する方法を採用しました。

factory :item_price_online, parent: :item_price do
  transient do
    skip_sales_channel { false }
  end

  sales_channel do
    if skip_sales_channel
      nil
    else
      create(:sales_channel, code: 'online')
    end
  end
end
context '販売チャネルが未入力の場合' do
  let(:subject_price) { FactoryBot.build(:item_price_online, skip_sales_channel: true) }
  it_behaves_like '無効', :sales_channel, '販売チャネルを入力してください'
end

transient 属性と条件付きブロックで Association を nil にできるようにした。

2. Redisのバージョン古い問題

発生した問題

sidekiq 7.2.4 から sidekiq 8.0.9 にアップデート後、sidekiq サーバーを起動すると下記エラーが発生しました。

You are connected to Redis 6.2.6, Sidekiq requires Redis 7.0.0 or greater

解決策

現在接続されている Redis は 6.2.6 だが、Sidekiq 8.0.9 は Redis 7.0 以上が必須だったので、Redis をバージョンする事によって解決しました。

Discussion