3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アップグレードガイド Rails 7.1.3 (jsbundling-rails with Webpack)

Last updated at Posted at 2024-03-03

はじめに

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)

      引用: https://techracho.bpsinc.jp/baba/2012_05_15/5462

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
    
  • 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のバンドル時にmanifest.jsonを出力する
        webpack.config.js
          plugins: [
            new WebpackManifestPlugin({
              fileName: 'manifest.json',
            }),
          ]
        
      • Webpack管理のファイルにアクセスがあった際にmiddlwareを通して、dev-serverのホストにリクエストを転送する
        • webpackerのコードが参考になります。
          イメージは、assets/webpack(webpack用のpath)にアクセスがあったら、webpack-dev-serverにリクエストを転送するmiddlewareを設定する。
    • 参考: https://tech.stmn.co.jp/entry/2021/01/15/161056

ガイド

基本的には上記のガイドで進められるがwebpack-dev-serverはサポートしていないのとmapについての言及がないので混乱する。

最後に

蛇足だったので書きませんでしたが、このprojectではreact_on_railsを使用しており、同時に脱react_on_railsしたので、その部分も上記の脱webpackerする前に行いました。要望があればもしかしたら追記するかもです。
かなり省略して書いたので、時間がある時に追記していきます。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?