はじめに
projectでRails6系からRails7.1.3へのアップグレードを対応することになり、ついでにwebpackerを剥がして、jsbundling-rails with Webpackに切り替えた際の備忘録を残します。
想定読者
- rails6系からrails7系にアップグレードする際、脱webpackerしたいけどwebpacker利用時の恩恵は残したいなと調べている人
Rails6系からRails7系へのアップグレード
コードに修正が必要になった変更点(rails-6.1.6 switch to rails-7.1.3)
enumの新構文が強制
- コード例
# before enum hoge: {1: fuga}, _prefix: "aaa" # after enum :hoge, {1: fuga}, prefix: "aaa"
テーブルにカラムの存在しないenum名の定義にはattribuitesの定義が必須
-
コード例
# before enum non_column_backed_example: {1: fuga}, _prefix: "aaa" # after attribute :non_column_backed_example, :integer enum :non_column_backed_example, {1: fuga}, prefix: "aaa"
-
問題になるパターン
migrationファイルでModelを呼び出すコードがあり、そのモデルに事後的にenumが追加されていた場合、migrationの途中で最終的にはテーブルにカラムが存在するenumの定義がnon_column_backed_exampleとして扱われる事象が発生する。migration1. companyテーブルの作成
migration2. Company.update_column(status: true)
=> この時点ではCompanyクラスにはenum :publish_status
が定義されているがcompanyテーブルにはpublish_statusカラムがまだ存在しないのでエラーが発生する
migration3. companyテーブルにpublish_status(enum)カラムを追加 -
対応
-
一次対応
enumメソッドをオーバーライドしてis_non_column_backedの際にattributeを呼び出すdef enum(name, values, **args) is_non_column_backed = attributes_to_define_after_schema_loads[name.to_s].nil? if is_non_column_backed data_type = values.values.first.is_a?(Integer) ? :integer : :string attribute(name, data_type) end super(name, values, **args) end
-
恒久対応
migrationではmodelを呼び出さないモデルクラスを使わない
migration内でデータの検査や変更をする場合など、ActiveRecordのモデルを使いたくなりますが、これは危険です。
migrationは基本的にずっと残るので、仮に将来そのモデルファイルが削除された場合、rake db:migrate:resetが通らなくなってしまいます。
SQLを直接使用するか、以下のように一時的にActiveRecord::Baseを継承したクラスを作るなどの対策が必要です。
Object.const_set "User", Class.new(ActiveRecord::Base)
-
2. BigDecimal(to_d)のround()の桁数が増えたため計算精度が上がった。
- 実際はrubyを3系に上げた時点で計算結果は変わっていたが、Rails6系ではBigDecimalをオーバーライドして以前のround()の桁数で実行されるようになっていたのでRails7系に上げたタイミングでBigDecimalの精度が上がった。
- コード例
# before 1.to_d/3.to_d =>0.3333333333333333333e0 # after 1.to_d/3.to_d => 0.333333333333333333333333333333333333e0
Psych::DisallowedClass error
-
ActiveRecord::Coders::YAMLColumnを使用したシリアライズ/デシリアライズ
-
safe_loadしたいclassはホワイトリスト(config.active_record.yaml_column_permitted_classes)に追加する
# serializeメソッドを使用した例 serialize :hoge, type: MyClass, coder: YAML # config/application.rb config.active_record.yaml_column_permitted_classes = [Hash, Symbol, MyClass] # << MyClassを追加する
-
-
GemでYaml.loadを使用している場合
最新versionでも対応されていない場合は、モンキーパッチが必要。
Sessionのオブジェクトに変更があり、RspecでのHashによるモックができない。
- create_session_mockヘルパーメソッドを作成して対応
呼び出しはこんな感じ
module SessionHelper def create_session_mock(data) # カスタムセッションクラスの定義 custom_hash_class = Class.new(Hash) do # ActionDispatch::Request::Sessionに似たメソッドを追加する def enabled? true end def loaded? true end end # 新しいカスタムセッションハッシュの作成 session = custom_hash_class.new # dataの内容をsessionに追加 data.each do |key, value| session[key] = value end session end end
let(:session) { create_session_mock({}) } allow_any_instance_of(ActionDispatch::Request).to receive(:session).and_return(session)
Rails 7.1でのActive RecordインスタンスのMarshal.dump
のシリアライズ方法に大きな変更が加えられました。
-
インスタンス変数がシリアライズに含まれなくなった
- changed?
# sessionを通すデータの受け渡しのエミュレート dump_instance = Marshal.dump(instance) load_instance = Marshal.load(dump_instance) # before load_instance.changed? => true # after load_instance.changed? => false
- attr_accessor
# sessionを通すデータの受け渡しのエミュレート instance.attr_accessor_A = "データがあります" dump_instance = Marshal.dump(instance) load_instance = Marshal.load(dump_instance) # before load_instance.attr_accessor_A => "データがあります" # after load_instance.attr_accessor_A => nil
- 他にもインスタンス変数に関わるものはあるかも
ガイド
基本的にはガイドに従って、適宜修正していく。
脱webpacker
jsbundling-railsの概要(webpacker目線)
- scriptタグ
javascript_pack_tag
はwebpackerで定義されたヘルパーメソッドなので、脱webpackerした際に使えなくなる。Railsのひとまずの対応としてはsprockets-railsのjavascript_include_tag
を使用すること。Webpack & Sprockets 構成が続くのがRailsの方針っぽい。 - assetsの公開
-
Webpackの出力をapp/assets/config/manifest.jsに読み込むことで、sprocketsに公開させる。
-
流れ: Webpackでバンドル → Sprocketsが(実はもう一度にバンドルしてる&)publicに出力して
localhost:3000/assets/application.js
などのpathでリクエスト可能にしている。 -
下記の場合はapp/assets/buildsに、Webpackの出力を書き込んでいる。
//= link_tree ../images //= link_directory ../stylesheets .css //= link manifest.json //= link_tree ../builds
-
- source-map
sproketsを通して公開することのデメリットとして2重でバンドルされることが挙げられる。
source-mapのurlがsproketsによって生成され最終行に付与される。だが実際はwebpackがsource-mapを生成しているが、sproketsのsourceMappingURLによって上書きされてしまう。// こっちはwebpackによって生成されたsourceMappingURL //# sourceMappingURL=http://localhost:3000/assets/packs/application-react.js-68840d1623831308455b5ffee7b7f258bbe9af8b6fd2979a13c32c2b459fcbb6.map ; // こっちはsprocketsによって生成されたsourceMappingURL->不要 //# sourceMappingURL=application-react.js-34bc9e7e31e414474d36f3bc9ebe5954be87a260253cbaa4cd4c6e7058b755c8.map
- 対応
config.assets.debug = true
の設定を削除することでsprocketsでsource-mapを生成することを防げる。 - issue: https://github.com/rails/jsbundling-rails/issues/73
- 対応
- webpack-dev-server
- jsbundling-rails
webpack-dev-serverのサポートはwebpackerが行なっていたので、jsbundling-railsからは標準ではwebpack-dev-serverはサポートされない。 - もしjsbundling-railsでもwebpack-dev-serverを使いたかったら下記の対応が必要
-
javascript_include_tag
をオーバーライド- webpack-dev-server起動時はsprocketsの管理から検索しないようにする
def javascript_include_tag(*source) if dev_server_runnning? super '', src: webpack_assets(*source) else super(super) end end def dev_server_runnning? # dev_serverにTCPリクエストしてアクセスが成功したらtrueを返す end def webpack_assets(*source) # dev_serverのmanifest.jsonを取得して、ファイル名からpathを返す # "application" => "/assets/application.js" end
- webpack-dev-server起動時はsprocketsの管理から検索しないようにする
- Webpackのバンドル時にmanifest.jsonを出力する
webpack.config.js
plugins: [ new WebpackManifestPlugin({ fileName: 'manifest.json', }), ]
- Webpack管理のファイルにアクセスがあった際に
middlware
を通して、dev-serverのホストにリクエストを転送する-
webpackerのコードが参考になります。
イメージは、assets/webpack(webpack用のpath)にアクセスがあったら、webpack-dev-serverにリクエストを転送するmiddlewareを設定する。
-
webpackerのコードが参考になります。
-
- 参考: https://tech.stmn.co.jp/entry/2021/01/15/161056
- jsbundling-rails
ガイド
基本的には上記のガイドで進められるがwebpack-dev-serverはサポートしていないのとmapについての言及がないので混乱する。
最後に
蛇足だったので書きませんでしたが、このprojectではreact_on_railsを使用しており、同時に脱react_on_railsしたので、その部分も上記の脱webpackerする前に行いました。要望があればもしかしたら追記するかもです。
かなり省略して書いたので、時間がある時に追記していきます。