By Patryk Antkiewicz, backend engineer at Unifa.
Recently I have been working on Ruby on Rails upgrade in one of our projects:
- Ruby: 2.7.2 -> 3.2.2
- Rails: 6.0.3.6 -> 7.1.2
I was proceeding according to the official guide , but during the process I encountered a few issues that were not mentioned in the document, so I would like to put them together here. It was API-only project without any frontend, so I didn't have to deal with various JS-related issues and the problems concerned mostly ActiveRecord.
I will skip the upgrade process itself here, and I will focus on some backward incompatible changes I discovered, which caused my RSpec tests to fail.
ActiveModel::Errors structure and behavior has changed
It seems there were some major changes in the implementation of ActiveModel::Errors class, so all interactions with it (especialy iterations) require special attention after upgrade.
'map' method behavior changed
First I experienced problems with the map method behavior. In one of generic Exception classes I had the following piece of code (e is ActiveRecord instance):
e.errors.map do |key, val| code_key = e.errors.details[key][0][:error] (...) end
Here are values of key and val within the loop for Rails 6:
(byebug) key :aim (byebug) val "は1000文字以内で入力してください"
And here are the same in Rails 7:
(byebug) key #<ActiveModel::Error attribute=aim, type=too_long, options={:count=>1000}> (byebug) val nil
Clearly the behavior of map has changed significantly - after looking at source code, the structure of new ActiveModel::Errors is quite different than in Rails 7. To achieve the same effect I had to refactor the code as follows:
e.errors.map do |err| key = err.attribute code_key = err.type (...) end
'add' method signature changed
In Rails 6 I was using the following code to add some custom validation error in ActiveRecord:
errors.add(:date, :date_outside_absence_range, { date: self.date })
However in Rails 7 it gives the following error:
ArgumentError: wrong number of arguments (given 3, expected 1..2)
Method signature is now different:
# Rails 6 add(attribute, message = :invalid, options = {}) # Rails 7 add(attribute, type = :invalid, **options)
Options is no longer a hash, so the method call had to be adjusted as follows:
errors.add(:date, :date_outside_absence_range, date: self.date)
ActiveRecord::Associations::Preloader usage has changed
In Rails 6 I was using Preloader as follows to load ActiveRecord associations and later perform some consistency validations:
Preloader.new.preload(child_profile_setting, [:child_profile_custom_settings])
However it no longer works - now the associations are passed in constructor, not in preload method, and the parameters need to be named. Instead of preload, the call method should be used:
Preloader.new(records: [child_profile_setting].flatten, associations: [:child_profile_custom_settings]).call
Useful Rubocop warnings
After applying the above fixes all of the tests were passing, so I could focus on Rubocop - I upgraded rubocop gem to 1.59.0 and enabled NewCops in rubocop.yml - now I had a few thousands of new warnings to deal with.
Some of them were really useful and helped me to write cleaner code, so I will list a few that were new to me:
Rails/CompactBlank
A cleaner way to filter out blank elements from a collection. Details: link
# bad excel_files.reject(&:blank?) # good excel_files.compact_blank
Performance/MapCompact
A cleaner equivalent for of calling .map.compact . Details: link
# bad records.map(&:some_method).compact # good records.filter_map(&:some_method)
Rails/WhereMissing
Simplifies queries that search for records that do not have some specific association. Details: link
# bad KidDevelopmentRecordSetting .left_joins(:kid_development_records) .where( kid_development_records: { id: nil } ) # good KidDevelopmentRecordSetting .where.missing(:kid_development_records)
Rails/ResponseParsedBody
A tiny style improvement for parsing Response body to JSON - in my case it was very common operation in RSpec tests. Details: link
# bad JSON.parse(response.body) # good response.parsed_body
Rails/EnvLocal
Useful method for all kind of configuration files which use different setup for local environments. Details: link
# bad Rails.env.development? || Rails.env.test? # good Rails.env.local?
Summary
Although the official Rails upgrade documentation is well written, it does not cover in detail the language features that are backward incompatible and need to be adjusted - sometimes we must learn them 'the hard way' during the process and look for solution. Having good test coverage of the project is extremely important during the upgrade proces, so that we can discover these incompatibilities immediately and avoid unpleasant surprises after deployment. Surprisingly, also Rubocop appeared to be very good way to learn some new, useful language features after the upgrade.
Unifa is actively recruiting, please check our website for details: