Upgrade From Ruby on Rails 3.2 to 4.2

by Alan Da Costa

ruby ruby-on-rails

With Ruby on Rails 3.x approaching it’s end of life for support, now is a great time to upgrade. Here’s a description of how I brought a client’s Ruby on Rails 3.2 application into the present with an upgrade to version 4.2.5 .

For applications with any non trivial amount of complexity, I suggest having tests. Hopefully you already have tests from TDD/BDD, but if not, backfilled tests are better than nothing. If you have nothing, good luck.

Bump The Rails Gem Version

Here is what I found in the Gemfile.

gem 'rails', '3.2.12'

Notice the fixed version? Unless there’s a special reason to only use this version of Rails, we should at least relax constraints for patch level updates. I did that with the spermy operator ~>.

gem 'rails', '~> 4.2'

Consider dropping the version specification altogether for the rails gem. Avoid including a version specification for any other gems in the Gemfile because it will make future upgrades more conflicted. Keep in mind, Gemfile.lock will record the exact version we’re using for all gems.

Attempt to Update Rails

Attempt? What about do or do not? Entangled gem dependencies might complicate an upgrade. Let’s introduce as little change as possible in order to break as little functionality as possible. I ran a command to only update the rails gem.

➥ bundle update rails
Fetching gem metadata from https://rubygems.org/............
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies...............
Bundler could not find compatible versions for gem "rails":
  In Gemfile:
    ie_conditional_tag (~> 0.5.0) ruby depends on
      rails (~> 3.0) ruby

    rails (~> 4.2) ruby

The update failed because the rails gem version requirement couldn’t be resolved. I tried to update to rails 4.2 but ie_conditional_tag wants rails (~> 3.0) ruby.

When I encounter this type of failure, my next steps are to either remove a version specification from the Gemfile (so the most current version can take hold), or remove/substitute the use of the old gem if there isn’t a newer replacement. Regardless of the outcome, I continued a cycle between running bundle update rails and fixing dependencies in the Gemfile.

After the previous failure, I returned to the Gemfile and removed the ~> 0.5.0 version specification for ie_conditional_tag, then ran bundle update rails. I later removed this gem from the project entirely because I found it wasn’t being used.

Here’s what the next pass looked like.

➥ bundle update rails
Fetching gem metadata from https://rubygems.org/............
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies............
Bundler could not find compatible versions for gem "railties":
  In Gemfile:
    sass-rails (~> 3.2.3) ruby depends on
      railties (~> 3.2.0) ruby

    chosen-rails (~> 1.0.0) ruby depends on
      railties (>= 3.0) ruby

    jquery-ui-rails (>= 0) ruby depends on
      railties (>= 3.1.0) ruby

    jquery-fileupload-rails (>= 0) ruby depends on
      railties (>= 3.1) ruby

    railties (< 5.0, >= 3.0) ruby
    ...

This failed command means another trip back to the Gemfile to remove a version specifications for sass-rails.

I repeated this process until the bundle update rails command succeeded.

Update Rails Configuration and Framework Files

Next, I updated configuration and framework loading files. Don’t race through this step too quickly because misconfiguration can cost much more time down the road. I updated a dozen or so files with the following command.

➥ bundle exec rake rails:update
    conflict  config/boot.rb
Overwrite .../config/boot.rb? (enter "h" for help) [Ynaqdh] Y
...

When conflicts arise, carefully analyze and update the replaced file as necessary. After overwriting conflicted files, I copied configuration lines with new keys or differing values from each old file to the new file.

Start the Rails Server

I started the Rails server, only to see failure.

➥ bundle exec rails s
=> Booting Thin
=> Rails 4.2.5 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
Exiting
.../dynamic_matchers.rb:26:in `method_missing': undefined method `whitelist_attributes=' for ActiveRecord::Base:Class (NoMethodError)
...

I resolved this error by including and configuring the protected_attributes gem. Expect additional failures from configuration values that you’ve copied into the updated configuration files but no longer exist. Fix these problems one at a time until they’re gone.

Route Failures

The next failures preventing the application from starting were due to incompatible route definitions.

➥ bundle exec rails s
=> Booting Thin
=> Rails 4.2.5 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
Exiting
../action_dispatch/routing/route_set.rb:549:in `add_route': Invalid route name, already in use: 'dashboard'  (ArgumentError)
You may have defined two routes with the same name using the `:as` option, or you may be overriding a route already defined by a resource with the same naming. For the latter, you can restrict the routes created with `resources` as explained here:
http://guides.rubyonrails.org/routing.html#restricting-the-routes-created
...

The old routes.rb contained several of these:

get 'dashboard' => 'locations#dashboard', :as => :dashboard
post 'dashboard' => 'locations#dashboard', :as => :dashboard

My fix was to remove the named route for the post, so it looked like:

get 'dashboard' => 'locations#dashboard', :as => :dashboard
post 'dashboard' => 'locations#dashboard'

Uninitialized Constants

Changing the major version of Ruby on Rails often means dependents will also change their major versions. With Semver, major version changes mean potential incompatibility with previous versions.

I ran into this problem and here is what it looked like:

Completed 500 Internal Server Error in 163ms (ActiveRecord: 14.8ms)

NameError - uninitialized constant Sprockets::Helpers:
  activesupport (4.2.5) lib/active_support/dependencies.rb:533:in `load_missing_constant'
  ...

Several files included this namespace. I looked at Sprockets and found Rails helpers had been extracted to sprockets-rails. When I looked closer into how the helpers from the namespace were used in the application, I was surprised to see there was nothing. This was dead code.

ActiveRecord/ActiveSupport API Changes

I could finally start the application server but the next error was right around the corner.

Completed 500 Internal Server Error in 18ms (ActiveRecord: 1.3ms)

ArgumentError - Unknown key: :uniq. Valid keys are: ...
  activesupport (4.2.5) lib/active_support/core_ext/hash/keys.rb:75:in `block in assert_valid_keys'
  ...

A few models had associations defined as such:

has_many :kits, through: :orders, uniq: true

This had to be replaced with:

has_many :kits, -> { distinct }, through: :orders

Other API Changes

I encountered view errors which looked like:

Completed 500 Internal Server Error in 35ms (ActiveRecord: 1.6ms)

TypeError - no implicit conversion of Symbol into Integer:
  actionview (4.2.5) lib/action_view/helpers/form_options_helper.rb:525:in `grouped_options_for_select'
  ...

View partials with this type of code:

<%= ff.select :state, grouped_options_for_select(Location::STATES, ff.object.state, "-- Select --") %>

Had to be changed to look like:

<%= ff.select :state, grouped_options_for_select(Location::STATES, ff.object.state, {prompt: "-- Select --"}) %>

In this case, grouped_options_for_select has changed to require an options parameter hash.

Win But Beware of Dragons

I won. My application was updated without any obvious errors, but there’s a chance I’ve missed something. My next steps are to have my team members review my code, and push for an extensive QA cycle. If all goes well, our production users won’t notice the upgrade :) .