こんにちは。ソリューション技術部プロフェッショナルサービス第2グループの渡邉です。今回はRuby, Railsのバージョンアップにあたり実施したことをまとめて紹介します。
実はこの会社に入る前に独学でRailsのWebアプリを作ったことがあり(6年以上前です)、もう一度開発し直してみようと思ったことがありました。その時のRailsのバージョンが4.2.8ですが、一方で、現在使用している端末がAppleシリコンのMacだったため当時のRails 4に適合したgemがインストールできず、結果起動できなかったため思い切ってRailsのバージョンを7に上げることにしました。その過程が結構な手間だったこともあり、同じような状況のエンジニアの方の一助になるかと思い記事にしました。また、レガシーサービスの保守をしている方でAppleシリコンのMacしか用意できず開発環境を用意するのが困難で無理くりDockerを使っている方にも一読頂ければと思います。
Ruby: 2.3.1 → 3.3.0
Ruby on Rails: 4.2.8 → 7.0.8
Rubyのバージョンアップ
Rubyのバージョンを上げます。rvmやrbenvを使っている方は以下を実行ください。
rvm
rvm install 3.3.0
rbenv
rbenv install 3.3.0 rbenv local 3.3.0
gemのバージョンアップ
Gemfileの中でバージョン指定しているgemの指定を一旦全て外します。Railsのみバージョンの修正が必要です。
- gem 'rails', '4.2.8' + gem 'rails', '7.0.8' - gem 'XXXX', '~> 3.3' + gem 'XXXX' - gem 'YYYY', '~> 5.1', '>= 5.1.0' + gem 'YYYY' etc.
その後Gemfile.lockを削除してbundle install
を実行ください。何度かエラーしますので頑張って最適なgemのバージョンを探してください。
なお、Rails 7の時点で非推奨になっているgemがいくつかあります。認識している限りは以下です。
Rails更新前 | Rails更新後 |
---|---|
therubyracer | mini-racer |
quiet_assets | (Railsに組み込まれたのでインストール不要) |
上記のtherubyracer, mini-racerはRubyの中からJavaScriptを実行するためのgemです。上で紹介しておいていながらですが、自分のアプリのGemfileにはmini-racerは入れていません。自分のアプリではモジュールバンドラにwebpackerを使っており、それ自体はwebpackというJavaScriptコンパイラをラッピングしたものであるため、webpackerを使っているならmini-racerは不要と言う判断です。
webpacker周りの修正
webpackerは5年以上もRailsで使われてきたモジュールバンドラです。ただし先に言うと、webpackerは開発を止めると公式に発表しており、Rails 7ではその代替手段としてjsbundling-railsの使用を推奨しています。
そのため本来ならwebpackerからjsbundling-railsへの移行方法を紹介すべきですが、諸都合でwebpackerを使い続けることにしています。jsbundling-railsはBun, esbuild, rollup.js, webpackのいずれかを使ってJavaScriptをバンドルする、という機能ですが、もともとwebpackを使っているのでwebpackを選びますし、jsbundling-railsの移行はまたのお楽しみにしようと思います。
ただ、webpackのバージョンを今回上げることにしました。最新のwebpack 5ではwebpackのmoduleをキャッシュしビルド時間を短縮することができるためです。 以下の公式サイトに方法が書かれています。
webpackのバージョンを上げるにあたり、webpack.config.jsファイルに修正が必要です。
+ const MiniCssExtractPlugin = require('mini-css-extract-plugin') - const ExtractTextPlugin = require('extract-text-webpack-plugin') module.exports = { ... plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css' + }), - new ExtractTextPlugin('[name].css'),
また、前述の通りwebpack 5からキャッシュの設定を細かく設定できるようになりました。 webpack 5での推奨設定は以下の通りです。
process.env.NODE_ENV = process.env.NODE_ENV || 'development' const webpack = require('webpack') + const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin') const {WebpackManifestPlugin} = require('webpack-manifest-plugin') module.exports = { + "cache": { + "type": 'filesystem', + "cacheDirectory": path.resolve(__dirname, '.temp_cache'), // キャッシュの保存場所を指定する + "buildDependencies": { + "config": [__filename] // 全ファイルの依存関係を配列にしたもの。 + } + },
importmapファイルの作成
Rails 7からはimportmap-railsが標準になり、新規アプリ作成時に自動でGemfileに入ってくるようになりました。このimportmap-railsのgemですが、jsファイルの中でimport文で他のjsやライブラリを読み込む際の読み込み元を明記することがこのgemを使うことでできるようになります。
これまでは、config/initializers/assets.rb
の中で
Rails.application.config.assets.paths << Rails.root.join("node_modules").to_s Rails.application.config.assets.paths << Rails.root.join("app", "javascript").to_s
と記述してから、app/assets/application.js
の中で
import "xxxx" import "yyyy"
と書くと、Rails.application.config.assets.paths
の読み込み先候補からxxxx.js, yyyy.jsを自動で探しにきてくれる挙動でした。
ただ読み込み候補の中で同名のファイルが複数あった場合どれが読み込まれるか分からず、開発やデバッグの手間が増えるでしょう。そこでimportmap-railsを使用すると、どのファイルを読み込むかを曖昧にせず明記できます。上記の例だと、config/importmap.rbという設定ファイルの中に以下のように記載します。
pin "xxxx", to: "https://www.npmjs.com/package/@xxxx/xxxx" pin "yyyy", to: "https://www.npmjs.com/package/@yyyy/yyyy"
こう書いた状態で、ターミナル上で
bin/importmap pin xxxx yyyy
を実行します。するとvendor/javascript
下にnpmパッケージの中で該当するjsが置かれ、import "xxxx"
した時にそのファイルが使われるようになります。
npmパッケージではなく自作のjsをimportする場合、例えばapp/javascripts
下にzzzz.jsを置いてimport文で読み込む場合は、
pin "zzzz", to: "zzzz.js"
とするだけでimport "zzzz"
で読み込むことができます。このようにimportmap.rbにモジュール指定子とjsファイルを紐づけることをピン留めする、というらしいです。
app/javascripts
下でさらにディレクトリがネストしている場合、例えばapp/javascripts/dir
下にaaaa.jsを置いてimportで使う場合、importmap.rbの記述は以下になります。
pin "aaaa", to: "dir/aaaa.js"
ちなみに私は、Rails 4からRails 7に上げた時にwebpackerのエントリーファイルに書いたimport "xxxx"
と書いた箇所で以下のエラーが出てこの対応をすることになりました。
Uncaught TypeError: Failed to resolve module specifier "xxxx". Relative references must start with either "/", "./", or "../".
既存のRailsアプリにimportmap-railsを追加する手順は以下です。
Gemfileに
gem 'importmap-rails'
を追記してからターミナルでbundle install
を実行ターミナルで
rails importmap:install
を実行。結果としてbin/importmap
とconfig/importmap.rb
が作成される。既存のpackage.jsonのdependenciesにあるnpmパッケージを一つ一つimportmap.rbに移植していく。移植方法は上記で紹介した通りです。
なお、残念ながらimportmap-railsはjsファイルのピン留めには対応していますが、cssファイルのピン留めはできません。
その他の細かい変更箇所
その他、バージョンアップに伴い変更が必要だったコードの修正内容を以下に紹介します。参考になれば幸いです。 なお以下のサンプルコードで、test_column, sample_flag, user_nameはカラム名、test_class, HogeClassはクラス名、TestTableはテーブル名、@searchはRansack::Search型のオブジェクト、@test_recordsはActiveRecord_AssociationRelation型のオブジェクトです。
モデルファイルに条件付きのバリデーションする時の条件文をブロックに入れなければなりません。
- validates :test_column, presence: true, if: "sample_flag" + validates :test_column, presence: true, if: lambda { "sample_flag" }
モデルファイルに関連付けの指定をclass_nameを宣言付きで行う場合はclass_nameを文字列にしないといけません。
- belongs_to :test_class, class_name: ::HogeClass + belongs_to :test_class, class_name: "HogeClass"
searchを使ったページネーションが使えなくなります。ransackを使わなければなりません。
Gemfile + gem 'ransack' コントローラー側 - @search = TestTable.search({"name_or_id_cont"=>"hogehoge"}) - @test_records = @search.result.order("id desc").page(1) + @search = TestTable.ransack({"name_or_id_cont"=>"hogehoge"}) + @test_records = @search.result.order("id desc").page(1) モデルファイル側 + def self.ransackable_attributes(auth_object = nil) + %w(name id) + end + def self.ransackable_associations(auth_object = nil) + [] + end
viewの一覧画面で特定のカラムでsortする時の記述が変わります。
- <%= sort_link @search, :user_name, {hide_indicator: true}, {class: "sorting"} %> + <%= sort_link @search, :user_name, {hide_indicator: true, class: "sorting"} %>
datetime型のカラムをview側に表示するかto_sで文字列化しようとするとWARNINGが発生します。今は問題ありませんが余裕があれば対応したほうが良いとおもいます。
- <td><%= test_record.created_at %></td> + <td><%= test_record.created_at.to_fs %></td>
最後に
今回はRails 4のWebアプリをRails 7にバージョンアップした時に行ったことをまとめて紹介しました。ただし、実際に行う作業はサービスによると思いますのであくまで参考程度にしていただけると幸いです。個人的感想として、バージョンはこまめにあげたほうがいいと実感しました。