ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Things I have learned from performing Rails upgrade

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:

unifa-e.com