homeASCIIcasts

415: Upgrading to Rails 4 

(view original Railscast)

Other translations: Ja Tr

Last week at RailsConf the first release candidate of Rails 4.0 was announced. This makes it a great time to try upgrading your own projects to Rails 4 so that you can see if they work and report any bugs that you find. We covered many of Rails 4’s new features in episode 400 so this episode will focus on the process of upgrading a Rails 3.2 app. This way you can follow along with one of your own applications.

The first step is to ensure that we’re running the latest version of Rails 3.2 and that all the application’s gems are up to date as well. Also we’ll need to be running Ruby 1.9.3 or later. It’s also a good idea to check that our app’s test suite is passing fully before starting to upgrade. The process will be more difficult if our app doesn’t have automated tests as it means that much more manual testing will be required. At the very least you should add some integration tests to our app before beginning the upgrade. Our application has tests which all pass so we can start to upgrade it to Rails 4. It’s a good idea to do this in a separate branch so we’ll create one now.

$ git checkout -b rails4

Next we’ll upgrade the version of Rails in the gemfile from 3.2.13 to 4.0.0.rc1 (or the current latest version). We’ll need to change the version numbers for some of the other gems too, including those in the assets group. Rails 4 removes this group so we can remove it and include the gems that were in it for all groups.

/Gemfile

gem 'rails', '4.0.0.rc1'
gem 'sqlite3'
gem 'sass-rails',   '~> 4.0.0.rc1'
gem 'coffee-rails', '~> 4.0.0.rc1'
gem 'uglifier', '>= 1.3.0'

In Rails 4 the production environment will not try to dynamically generate any assets by default and use the static precompiled assets and to go along with this change the assets are now intended to be precompiled in the production environment. If you’re running rake assets:compile to precompile the assets you’ll need to set the environment to production first.

$ RAILS_ENV=production bundle exec rake assets:precompile

A nice side-effect of this is change is that it simplifies the application’s config file. In Rails 3 we had to load the gems matching the assets group, but only in the development and test environments. This means that we can replace this code:

/config/application.rb

if defined?(Bundler)
  # If you precompile assets before deploying to production, use this line
  Bundler.require(*Rails.groups(:assets => %w(development test)))
  # If you want your assets lazily compiled in production, use this line
  # Bundler.require(:default, :assets, Rails.env)
end

with this:

/config/application.rb

Bundler.require(:default, Rails.env)

If you miss the assets group in your production setup you can add something like that on your own and customize the group loading behaviour in this file.

We can try this now by running bundle update to install the new version. This appears to work successfully but the command can sometimes have some unexpected behaviour. We can see some of this by running bundle outdated which is a little-known command but which is useful for finding out of date gems.

$ bundle outdated
Fetching gem metadata from https://rubygems.org/.........
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Outdated gems included in the bundle:
  * builder (3.2.0 > 3.1.4)
  * paper_trail (2.7.1 > 1.6.4)

When we run this for our application it finds two outdated gems. The version of builder is fairly close but that for paper_trail is way off and this is because it couldn’t find a newer version compatible with Rails 4. This is one reason why it’s important to be strict in the gemfile about the versions we’ll accept. We haven’t specified a version for paper_trail in the gemfile so we’ll do that now.

/Gemfile

gem 'paper_trail', '~> 2.7.1'

When we run bundle update now we get an error message about a version conflict. Papertrail expects a version of ActiveRecord around 3.0 so we’ll have to find a different version that’s compatible with Rails 4. Papertrail has a Git branch for Rails 4 and we can use that in the Gemfile, but this may not apply for other gems so we’ll need take a look at each affected gem’s issue tracker to see what its status is.

/Gemfile

gem 'paper_trail', github: 'airblade/paper_trail', branch: 'rails4'

When we run bundle update now it will fetch a version of Papertrail from the Github project and it completes successfully.

Now that we have our gems up to date we can try running our specs. When we do so they fail miserably. The output shows an error saying that the caches_page method in the EpisodesController is undefined. We’re using page caching in our application which has been removed in Rails 4, along with other things such as observers, protected model attributes and ActiveResource. The good news is that if we need any of these features, it’s easy to add them back through gems. We’ll add a list of items that it’s a good idea to use during the upgrade process.

/Gemfile

gem 'protected_attributes'
gem 'rails-observers'
gem 'actionpack-page_caching'
gem 'actionpack-action_caching'
gem 'activerecord-deprecated_finders'

Doing this removes any errors related to the functionality that’s been extracted from Rails 4. When we’re doing a major version upgrade like this it’s important to get the application into a working state as quickly as possible so it’s best to avoid any large refactorings or other large changes to the app and adding these gems helps with that. When our tests pass we can then start to clean up the code and maybe change it to use techniques that don’t rely on these gems. We’ll run bundle to install these gems then try running our specs again to see if we can get them to pass. This time, along with a number of deprecation warnings, we see an error related to the routes.

$ spec .
...
You should not use the `match` method in your router without specifying an HTTP method.
If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.
If you want to expose your action to GET, use `get` in the router:
  Instead of: match "controller#action"
  Do: get "controller#action"
...

The match method is no longer accepted. Normally we should just replace this with get but if we want to support multiple methods we can add them or use match with the via: option.

/config/routes.rb

# match 'new', to: 'episodes#new', via: [:get, :post]
get 'new', to: 'episodes#new'

When we try running our specs now they run but there are many failures, most of which are about mass-assigning protected attributes including ones for the Version model from Papertrail. This should run in Rails 4 with strong parameters but we’re still using protected attributes. We can get around this by modifying our app’s config file and setting active_record.whitelist_attributes setting to false. By default it’s set to true which means that it expects attr_accessible to be defined in every model which probably won’t be the case while we’re migrating our application to use strong parameters.

/config/application.rb

config.active_record.whitelist_attributes = false

Now our specs all pass, although there are still a lot of deprecation warnings. Let’s walk through these and try to sort them out. Many of them have to do with configuration options such as whiny_nils, which are no longer required. In the development config file we’ll remove the line related to whiny_nils, and add an eager_load option, which we’ll set to false. We can also remove the other options which are no longer required.

/config/development.rb

Screencaster::Application.configure do
  # Settings specified here will take precedence over those in config/application.rb
  # In the development environment your application's code is reloaded on
  # every request. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  config.cache_classes = false
  config.eager_load = false
  # Show full error reports and disable caching
  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false
  # Don't care if the mailer can't send
  config.action_mailer.raise_delivery_errors = false
  # Print deprecation notices to the Rails logger
  config.active_support.deprecation = :log
  # Expands the lines which load the assets
  config.assets.debug = true
end

Next we’ll configure the production environment. Here we need to turn on eager loading and alter the config.assets.compress option.

/config/production.rb

config.eager_load = true
 # Compress JavaScripts and CSS
 config.assets.js_compressor = :uglifier

In the test environment we need to remove the whiny_nils option and set eager loading to false. We can also remove the mass_assignment_sanitizer as we’ll be transitioning to strong parameters.

/config/test.rb

# Log error messages when you accidentally call methods on nil
# config.whiny_nils = true
config.eager_load = false
# Raise exception on mass assignment protection for Active Record models
# config.active_record.mass_assignment_sanitizer = :strict

A quick note now about the asset pipeline. If we look in our application’s config file we’ll see a leftover config.assets.version option that’s set to true. This is no longer necessary as in Rails 4 the asset pipeline is enabled by default so we’ll need to set it to false if we’re not using the asset pipeline in our application. If we are we can just remove this option.

There’s one more configuration change we need to make, in the secret token initializer. In Rails 4 the configuration option in this file has been renamed from secret_token to secret_key_base. We’ll need to specify both options while we’re transitioning from Rails 3 but once we’ve successfully migrated our application we can remove the secret_token option. It’s best to use a different token for our secret_key_base.

/config/initializers/secret_token.rb

Screencaster::Application.config.secret_token = '762a2f23950e306261908d4e5519ffe71ce626b119e9fc03a012ba86f46d82ef32d72f283633bacc2f59cf94ce5968552fe97d157e7f00560c1217d4592dda09'
Screencaster::Application.config.secret_key_base = 'xx762a2f23950e306261908d4e5519ffe71ce626b119e9fc03a012ba86f46d82ef32d72f283633bacc2f59cf94ce5968552fe97d157e7f00560c1217d4592dda09'

This is necessary because we’re moving from a serialized cookie stored on the client to an encrypted cookie. This prevents users from easily being able to see the contents of their session cookies.

We’ve made a lot of configuration changes here but we haven’t covered everything. We’ll create a new Rails 4 application so that we can look at the generated config files and then compare them with those of the application we’re upgrading to see what else we need to copy over.

With all these changes in place we’ll try running our specs again to make sure that we haven’t broken anything and to see if we’ve reduced the number of deprecation errors. All the tests still pass but there are still deprecation warnings so we’ll see if we can reduce them. One of them is inside the Episode model. Whenever we defined a named scope we now need to pass a callable object as a second object, such as a lambda, like this:

/app/models/episode.rb

scope :published, -> { where('published_on <= ?', Time.now.to_date) }

This is necessary because it’s east to set up a scope with something dynamic in it, such as the current time. Without the lambda this would always use the time at which the class is loaded, rather than the time at which the scope is called each time. The other deprecation we’re still getting is in the EpisodesController’s index action.

/app/controllers/episodes_controller.rb

def index
  @episodes = Episode.published.find_all_by_pro(false)
end

Dynamic finder methods like this are no longer supported. Instead we should use the find method and pass the field in as an option.

/app/controllers/episodes_controller.rb

def index
  @episodes = Episode.published.where(pro: false)
end

When we run our specs now they all pass with no deprecation warnings.

Now that we’ve cleaned up the deprecations we can focus on other transitions such as strong parameters. These were covered in episode 371 and are basically a way to move mass assignment restrictions out of the model so that instead of using attr_accessible there we put the restrictions in the controller. We’ll remove the attributes from the Episode model.

/app/models/episode.rb

#attr_accessible :description, :name, :seconds, :published_on, :timecode

In the controller we’ll define a private method called episode_params. This is the conventional way to do this, although there are others we could use.

/app/controllers/episodes_controller.rb

def episode_params
  params.require(:episode).permit(:description, 
  :name, :seconds, :published_on, :timecode)
end

In this method we call permit on the parameters that are passed in and pass it the permitted parameters. We use params.require(:episode) instead of params[:episode] to ensure that the parameters hash is available and to avoid a nil exception if they are not. We can then call this method whenever we get the parameters from the form.

/app/controllers/episodes_controller.rb

def create
  @episode = Episode.new(episode_params)
  if @episode.save
    redirect_to @episode, notice: 'Episode was successfully created.'
  else
    render action: "new"
  end
end

Once we’ve done this everywhere we can remove the protected_attributes gem from the gemfile. We can also remove any config options related to it from the app’s config file such as config.active_record.whitelist_attributes.

We’ve managed to remove one of the transition-related gems from our application now but it might be tricky to remove the others. We could use callbacks instead of rails-observers while page_caching still has some good use-cases so we could keep using this. HTTP caching, covered in episode 321, is a better alternative to action_caching and we can remove activerecord-deprecated_finders as it is, in fact, a dependency in Rails 4.0, although it will be removed from Rails 4.1.

Finally we’ll mention a few minor changes that we can make. The first is in the controllers where before_filter is now before_action. There isn’t yet a deprecation warning but the new name is a little clearer. In the routes file if we use put here we can instead call patch as this better fits updating a record.

If we have an app with a /test directory the structure has changed a little so it’s worth taking a look at a new Rails 4 app to see what’s different. Also the /vendor/plugins is no longer used so we can move the plugins out into a gem or into /lib.

There are many more Rails 4 features that we haven’t covered here, but which have been covered in past episodes. There are also a number of resources listed in the blog post that announces the release candidate.