415: Upgrading to Rails 4
(view original Railscast)
Other translations:
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.