Our Rails’ version was 4.2 and we needed to upgrade to a later version mostly because Heroku stack requires it.
There are many posts and instructions out there, describing how the upgrade process should be done. There is, of course, the official rails documentation (which is great!), and a lot of other great posts (which can be found in the resources section below).
You’re probably wondering… why do we need another post?
Well, as much as I searched, there was no single post that describes how to actually perform the upgrade, step by step, with all the issues you might run into along the way. I chose to write this post to describe my upgrade process and mostly emphasise the issues I had to tackle along the way, which are not written in the official manual, but hidden in rails’ source code, other gems, stack-overflow questions or other sources.
Starting from the beginning, and how naive I was. Upgrading rails? Sure, no problem. At first, I just changed the rails version in my Gemfile
gem ‘rails’, ‘5.1.5’
and then simply ran this command to install the dependencies specified in my Gemfile:
>> bundle install
Oh wait, I forgot to actually update the rails’ version
>> bundle update rails
At that point, I got a million (no, not really a million) errors indicating my gems were incompatible with the new Rails version. I should have seen it coming, right? But there are so many of them.
Let’s try to update them all to the latest available version:
>> bundle unpdate
Easy, right?
Great, now it’s testing time. Trying to run the server…
>> rails server
Boom. Nothing works.
At that point, I had so many changes in my code (created when I tried to fix some of the errors manually) and so many gems were updated (most of them unnecessarily) that I had no idea where to continue my investigation. I decided to do the smart thing and do the whole process again, but wisely this time.
Step #1: Upgrading the Gems
I chose to take the time and divide the work into small chunks of gem upgrades. I used ‘is ready to rails 5?’ to understand which gems had to be upgraded. For the gems that were not provided there, I used rubygems to investigate their dependencies and tried to understand whether they could be used in rails 5 or not.
Some of the gems are already built-in rails 5 (such as ‘rails-api’, ‘test_after_commit’), so you might be aware of those as well.
Upgrading the gems was done in 3 chunks, with each including up to 10 gems. After testing locally, the upgrade was tested for a while in a staging environment, and only then was merged to production. The intent was to minimise the risks.
At this point you must be wondering what about the other gems and how do I know if an upgrade is needed? To be honest, I decided to let future Eti deal with that.
Step #2: Inherit from ApplicationRecord instead of ActiveRecord::Base
Starting from Rails 5.0, all models inherit from ApplicationRecord which is a new superclass for all app models, similar to app controllers subclassing. This gives apps a single spot to configure app-wide model behaviour.
Practically, that means that I had to create an ApplicationRecord class and make all models inherit from it. Pretty straightforward.
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
Step #3: Tag migration with its created Rails version
ActiveRecord migrations now need to be tagged with the Rails version they were created under. One of the reasons for this is that strings in Rails 4.2 have a default size of 4 bytes. In Rails 5.0, their default size is now of 8 bytes.
In my case, all the migrations were created with Rails 4.2, so I added this version to all of them.
class BuildBasicSchema < ActiveRecord::Migration[4.2]
Step #4: Be aware that ActionController::Parameters no longer inherits from HashWithIndifferentAccess
Calling params at your controllers will no longer return a hash, but an ActionController::Parameter object:
> params
=> <ActionController::Parameters {“id”=>”123", “controller”=>”users”, “action”=>”create”} permitted: false>
This change should not affect accessing the keys in the params hash like params[:some_key], but you should pay attention when using any one of these methods: any?, map, symbolize_keys, dig, or any other method that depends on being able to read the hash regardless of permitted? and remember to permit the parameters first and then convert it to a hash.
params.permit([:params, :to, :permit]).to_h
Step #5: Change commands’ directory
At this point, I decided to try my luck, and run rails server.
Disappointed, I had to solve this error that popped up:
config/boot.rb:6:in `require’: cannot load such file — rails/commands/server (LoadError)
After googling a bit, I discovered that the rails’ authors changed the commands directory structure, so instead of finding the wanted command at rails/command/server it was now located at rails/command/server/server_command.
In ‘config/boot.rb’, I required these directories:
require ‘rails/commands/server/server_command’
You can also check the rails repository and the new command structure here.
Step #6: Remove some deprecated or deleted methods
remove raise_in_transactional_callbacks since it is no longer supported. The following was deleted from the application’s config file.
# Do not swallow errors in after_commit/after_rollback callbacks.
config.active_record.raise_in_transactional_callbacks
Step #7: Define the callback before usingskip_before/after/around_action
skip_before_action, skip_after_action, and skip_around_action will now raise an ArgumentError if the specified callback doesn’t exist.
If you wish to keep the current behaviour, you can prevent these errors from showing with ‘raise: false’ and that lets you opt out of the new behaviour.
skip_before_action :verify_authenticity_token, raise: false
That’s it. The server ran successfully. Now what?
I used postman to test our endpoints to make sure I got the wanted responses and it seemed like the application looked pretty much ok.
Moving forward, I started to run our specs beginning with the controllers. I followed one rule — the fixes should be in the code and not in the tests, except for the next step:
Step #8: Fix the Controllers’ specs
HTTP request methods will accept only the following keyword arguments: params, headers, env, xhr, format. So I had to add params to all my tests
Step #9: Make sure not to pass ActiveRecord to .find method
If you do so, you’ll probably encounter the following error:
You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`
So, as the message indicates, try to pass the object’s ID instead of its instance.
Below is an example from my tests:
Step #10: Make sure to specify a response in all controllers’ actions
In Rails 5, controller action responds with ‘No content’ (204) by default, unless a response is specified. This great post explains the subject.
And that’s it.. It’s done. Our Rails 5.1.5 app is up and running!
Know some other steps I missed? Share with me in the comments section.