Upgrade Rails from 3.2 to 4.0

Upgrade Rails from 3.2 to 4.0

This article is part of our Upgrade Rails series. To see more of them, click here.

A previous post covered some general tips to take into account for this migration. This article will try to go a bit more in depth. We will first go from 3.2 to 4.0, then to 4.1 and finally to 4.2. Depending on the complexity of your app, a Rails upgrade can take anywhere from one week for a single developer, to a few months for two developers.

  1. Preparations
  2. Ruby version
  3. Gems
  4. Config files (config/)
  5. Application code
  6. Models (app/models/)
  7. Controllers (app/controllers/)
  8. Active Support
  9. Tests
  10. Miscellaneous
  11. Next steps

1. Preparations

Before beginning with the upgrade process, we have some recommended preparations:

  • Your Rails app should have the latest patch version before you move to the next major/minor version.
  • You should have at least 80% test coverage unless you have a dedicated QA team.
  • Follow a Git flow workflow to actively manage at least two environments: staging and production.
  • Check your Gemfile.lock for incompatibilities by using RailsBump.
  • Create a dual boot mechanism, the fastest way to do this is installing the handy gem next_rails.

For full details check out our article on How to Prepare Your App for a Rails Upgrade.

2. Ruby version

Rails 3.2.x is the last version to support Ruby 1.8.7. If you’re using Ruby 1.8.7, you’ll need to upgrade to Ruby 1.9.3 or newer. The Ruby upgrade is not covered in this guide, but check out this guide for more details on that.

3. Gems

You can add the aptly named rails4_upgrade gem to your Rails 3 project’s Gemfile and find gems which you’ll need to update:

➜  myproject git:(develop) ✗ bundle exec rake rails4:check

** GEM COMPATIBILITY CHECK **
+------------------------------------------+----------------------------+
| Dependency Path                          | Rails Requirement          |
+------------------------------------------+----------------------------+
| devise 2.1.4                             | railties ~> 3.1            |
| devise-encryptable 0.2.0 -> devise 2.1.4 | railties ~> 3.1            |
| friendly_id 4.0.10.1                     | activerecord < 4.0, >= 3.0 |
| strong_parameters 0.2.3                  | actionpack ~> 3.0          |
| strong_parameters 0.2.3                  | activemodel ~> 3.0         |
| strong_parameters 0.2.3                  | activesupport ~> 3.0       |
| strong_parameters 0.2.3                  | railties ~> 3.0            |
+------------------------------------------+----------------------------+

Instead of going through your currently bundled gems or Gemfile.lock manually, you get a report of the gems you need to upgrade.

4. Config files

Rails includes the rails:update task. You can use this task as a guideline as explained thoroughly in this post. It will help you get rid of unnecessary code or monkey-patches in your config files and initializers, specially if your Rails 3 app was running on Rails 2.

As an alternative, check out RailsDiff, which provides an overview of the changes in a basic Rails app between 3.2 and 4.0 (or any other source/target versions).

If you’re feeling adventurous, you can give this script a try. It attempts to apply this git patch (similar to the patch shown on RailsDiff) to your Rails app to migrate from 3.2 to 4.0. However, I don’t recommend this for complex or mature apps, as there will be plenty of conflicts.

  • Thread-safe by Default

In Rails 4 applications, the threadsafe! option will be enabled by default in production mode. The way to turn it off will be by setting config.cache_classes and config.eager_load to false.

config.threadsafe! is deprecated in favor of config.eager_load which provides a more fine grained control on what is eager loaded.

This shouldn’t be much of a problem, unless the app (or one of its gems) is depending too much on thread safety.

Here is a great blogpost by Aaron Patterson explaining this change and it’s consequences:

config.threadsafe!: What does it do?

  • Eagar loading

In Rails 4 eager loading is controlled by config.cache_classes and config.eager_load.

1) Set the eager load value as per the below warning in each environment

2) If you have not defined config.eager_load then it will use config.cache_classes value.

if config.eager_load.nil?
          warn <<-INFO
                config.eager_load is set to nil. Please update your config/environments/*.rb files accordingly:

                  * development - set it to false
                  * test - set it to false (unless you use a tool that preloads your test environment)
                  * production - set it to true

                INFO
      config.eager_load = config.cache_classes
end

5. Application code

a. Models

  • All dynamic finder methods except for .find_by_... are deprecated:
# before:
Authentication.find_all_by_provider_and_uid(provider, uid)

# after:
Authentication.where(provider: provider, uid: uid)

You can regain usage of these finders by adding the gem activerecord-deprecated_finders

  • ActiveRecord scopes now need a lambda:
# before:
default_scope where(deleted_at: nil)

# after:
default_scope { where(deleted_at: nil) }

# before:
has_many :posts, order: 'position'

# after:
has_many :posts, -> { order('position') }

# before:
scope :fulfilled,   where{ fulfilled_at != nil }

# after:
scope :fulfilled,   -> { where{ fulfilled_at != nil }}

(Friendly reminder: beware when using default_scope)

  • Protected attributes is deprecated, but you can still add the protected_attributes gem. However, since the Rails core team dropped its support since Rails 5.0, you should begin migrating your models to Strong Parameters anyway.

To do so, you will need to remove calls to attr_accessible from your models, and add a new method to your model’s controller with a name like user_params or your_model_params:

class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:name, :email)
  end
end

Finally, change (most) references to params[:user] to user_params in your controller’s actions. If the reference is for an update or a creation, like user.update_attributes(params[:user]), change it to user.update_attributes(user_params). This new method permits using the name and email attributes of the user model and disallows writing any other attribute the user model may have (like id).

  • ActiveRecord Observers were removed from the Rails 4.0 codebase and extracted into a gem. You can regain usage by adding the gem to your Gemfile:
gem 'rails-observers' # https://github.com/rails/rails-observers

As an alternative, you can take a look at the wisper gem, or Rails’ Concerns (which were added in Rails 4.0) for a slightly different approach.

  • ActiveResource was removed and extracted into its own gem:
gem 'activeresource' # https://github.com/rails/activeresource
  • Association names must be a Symbol
# before:
has_one "report_#{report}", class_name: 'MyReport'

# after:
has_one :"report_#{report}", class_name: 'MyReport'
  • Add multiline: true to validates_format_of when we use regular expression

Due to frequent misuse of ^ and $, you need to pass the multiline: true option in case you use any of these two anchors in the provided regular expression. In most cases, you should be using \A and \z.

# before:
validates_format_of :recipient_email, with: /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i

# after:
validates_format_of :recipient_email, with: /^[^@][\w.-]+@[\w.-]+[.][a-z]{2,4}$/i, multiline: true
  • conditions attribute is replaced by lambda function with where clause
# before
belongs_to :valid_coupon, :conditions => 'discounts > #{payments_count}'
# after:
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }
  • Active model serializers changes

The option include_root_in_json controls the top-level behavior of as_json. If true, as_json will emit a single root node named after the object’s type.

In Rails 3, it is set to true,

In Rails 4, it is set to false by default. To enable it add following in config file. ActiveRecord::Base.include_root_in_json = true More details

# before:
user.as_json
=> { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
            "created_at": "2006/08/01", "awesome": true} }

# after:
user.as_json
=> { "id" => 1, "name" => "Konata Izumi", "age" => 16,
     "created_at" => "2006/08/01", "awesome" => true}
  • ActiveRecord custom attributes (attributes not stored in the database) no longer allowed. More details
# before: Previously we would just silently write the attribute.
attr_accessible :months
object.months = "Test"

# After:
attr_accessible :months
object.months = "Test" # Raise exception - can't write unknown attribute

# Workaround
after_initialize do
  self.months = nil unless @attributes.key?("months")
end

def months
  @attributes["months"]
end

def months=(value)
  @attributes["months"] = value
end
  • extend association changes

1) With only extend attribute

# before:
belongs_to :diagnosis, :extend => DiagnosisAssociationExtension

# After:
belongs_to :diagnosis, -> { extending DiagnosisAssociationExtension }

2) With extend and conditions attributes

# before:
belongs_to :clinic,  primary_key: :person_id, foreign_key: :person_id,
    conditions: conditions_proc,  extend: MultiAssociation::OnTestId

# After:
belongs_to :clinic,  -> { extending(MultiAssociation::OnTestId).where(conditions_proc) }, primary_key: :person_id, foreign_key: :person_id

b. Controllers

  • ActionController Sweeper was extracted into the rails-observers gem, you can regain usage by adding the gem to your Gemfile:
gem 'rails-observers' # https://github.com/rails/rails-observers
  • Action caching was extracted into its own gem, so if you’re using this feature through either:
caches_page   :public

or:

caches_action :index, :show

You will need to add the gem:

gem 'actionpack-action_caching' # https://github.com/rails/actionpack-action_caching
gem 'actionpack-page_caching'

b. Active Support

BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger from Ruby standard library.

6. Tests

From Ruby 1.9.x onwards, you have to include the test-unit gem in your Gemfile as it was removed from the standard lib. As an alternative, migrate to Minitest, RSpec or your favorite test framework.

7. Miscellaneous

  • Routes now require you to specify the request method, so you no longer can rely on the catch-all default.
# change:
match '/home' => 'home#index'

# to:
match '/home' => 'home#index', via: :get

# or:
get '/home' => 'home#index'
  • Rails 4.0 dropped support for plugins, so you’ll need to replace them with gems, either by searching for the project on RubyGems/Github, or by moving the plugin to your lib directory and require it from somewhere within your Rails app.

8. Next steps

If you successfully followed all of these steps, by now you should be running Rails 4.0!

To fine-tune your app, check out FastRuby.io, and feel free to tell us how your upgrade went.

If you’re not on Rails 4.0 yet, we can help! Download our free eBook: The Complete Guide to Upgrade Rails.

Get the book