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, check our article title The Rails Upgrade Series opens a new window .

A previous post opens a new window 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 opens a new window 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. Tests
  7. Miscellaneous
  8. Next steps

1. Preparations

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

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

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 opens a new window for more details on that.

It is recommended to upgrade to the latest compatible version of Ruby before upgrading Rails. Depending on your current Rails version you can upgrade to Ruby 2.0 or Ruby 2.2. You can use our compatibility table opens a new window to find out.

3. Gems

You can use next_rails opens a new window ’s bundle_report feature to check the compatibility of your gems with the next version of Rails:

$ bundle_report compatibility --rails-version=4.0.13

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

Note that next_rails requires Ruby 2.0 or newer to work. If upgrading to Ruby 2 is not possible for any reason, you can use this gem opens a new window for a similar output.

4. Config files

Rails includes the rails:update task opens a new window . You can use this task as a guideline as explained thoroughly in this post opens a new window .

It will help you get rid of unnecessary code or monkey-patches in your config files and initializers, especially if your Rails 3 app was running on Rails 2.

You should not blindly commit the changes made by rails:update, you should analyze what makes sense for your app in case something requires extra changes.

As an alternative, check out RailsDiff opens a new window , 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 opens a new window a try. It attempts to apply this git patch opens a new window (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 article by Aaron Patterson explaining this change and its consequences: config.threadsafe!: What does it do? opens a new window

  • Eager loading

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

Set the eager load value as per the below warning in each environment. If you have not defined config.eager_load then it will use the value of config.cache_classes.

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
  • config.assets.compress = true directive no longer works in Rails 4.

In Rails 4, the preferred way to enable asset compression is to use a JavaScript or CSS compressor directly.

For example, you can use the uglifier gem to compress JavaScript files, or the sass-rails gem to compress CSS files.

In your config/application.rb file, add the following configuration:

config.assets.js_compressor = :uglifier
config.assets.css_compressor = :sass

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)

Rails 4.0 includes the activerecord-deprecated_finders opens a new window gem as a dependency to support both the old and new 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 IS NOT NULL')

# after:
scope :fulfilled, -> { where('fulfilled_at IS NOT NULL') }

(Friendly reminder: beware when using default_scope opens a new window )

  • The :finder_sql association option is deprecated in favor of scopes

Note: As of 4.1, :finder_sql is completely REMOVED from Rails opens a new window

# before:
class EmailInvitation < ActiveRecord::Base
  belongs_to :user, finder_sql: "SELECT * FROM users WHERE email_invitation_id = #{id}"
end

# after:
class EmailInvitation < ActiveRecord::Base
  belongs_to :user, -> (o) { where(email_invitation_id: o.id) }
end

However, since the Rails core team dropped its support since Rails 5.0, you should begin migrating your models to Strong Parameters opens a new window 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).

If you want to reduce the amount of manual work, you can use our open source Ruby gem for automatically doing all this for you: rails_upgrader opens a new window :

gem install rails_upgrader
rails_upgrader go

This won’t get you all the way there, but it will do 90% of the work.

  • 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 opens a new window , 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
  • The following options in your has_many, has_one, or belongs_to associations are deprecated:
:order, :limit, :uniq, :readonly, :conditions

All of these options can be replaced by a scope wrapped in a lambda passed as the second argument of the association:

# before:
has_many :donors, :uniq => true
has_many :donors, :readonly => true
has_many :donors, :through => :donations, :uniq => true, :order => "name", :conditions => "age < 30", :readonly => true, limit: 10

# after:
has_many :donors, -> { uniq }
has_many :donors, -> { readonly }
has_many :donors, -> { where("age < 30").order("name").limit(10).readonly.uniq }, :through => :donations

When the conditions there are different scenarios to consider apart from a static string mentioned above:

When using a string with interpolated attributes

# before
belongs_to :valid_coupon, :conditions => 'discounts > #{payments_count}'

# after:
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }

When using a proc does not depend on the current record

# before:
has_one  :user, conditions: proc{ [ "created_at >= ?", Time.zone.now ] }, order: 'created_at DESC'

# after:
has_one  :user, -> { where("created_at >= ?", Time.zone.now).order('created_at DESC') }

When using a proc that depends on the current record

# before:
condition_proc = proc {
  # code that depends on `self`
}

belongs_to :user, conditions: condition_proc

In Rails 4, you have to execute the proc block first with a named parameter instead of self and then pass the output to the where query like this:

# after:
condition_proc = proc do |object|
  # replace uses of `self` with `object`
end

belongs_to :user, ->(object) { where(condition_proc.call(object)) }
  • 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 the following in a config file. ActiveRecord::Base.include_root_in_json = true More details opens a new window

# 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}
# 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

With only the extend option

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

# after:
belongs_to :diagnosis, -> { extending(DiagnosisAssociationExtension) }

With extend and conditions options

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

# after:
belongs_to :clinic,  -> { extending(MultiAssociation::OnTestId).where(some_conditions) },
    primary_key: :person_id, foreign_key: :person_id
  • Model.scoped is deprecated in favor of Model.all:
# before:
User.scoped

# after:
User.all
  • DEPRECATION WARNING: The :distinct option for Relation#count is deprecated. Please use Relation#distinct instead. (eg. relation.distinct.count).
# before:
Model.where(...).count(distinct: true)

# after:
Model.where(...).distinct.count
  • increment_open_transactions is deprecated and has no effect.

As of Rails 4.1, it’s removed from Rails opens a new window

It usually requires no changes. It’s not a common API to use. If the application or a custom gem depends on this, an alternative must be analyzed for those specific cases.

  • The touch method can not be called on new records

Do not call .touch on new records. If you do, you will get a ActiveRecord::ActiveRecordError: can not touch on a new record object error because touch should be used to update the timestamps for persisted objects only.

Add a unless new_record? when needed.

object.touch unless object.new_record?
  • Bidirectional destroy dependencies

In Rails 4, if you defined a bidirectional relationship between two models with destroy dependencies on both sides, a call to destroy would result in an infinite callback loop SystemStackError: stack level too deep.

Take the following relationship.

class Content < ActiveRecord::Base
  has_one :content_position, dependent: :destroy
end

class ContentPosition < ActiveRecord::Base
  belongs_to :content, dependent: :destroy
end

Calling Content#destroy or ContentPosition#destroy would result in an infinite callback loop.

Solution: We can fix this by removing one of the dependent: :destroy options.

In most cases, removing the options from the belongs_to association prevents the issue, though it’s important to ensure this aligns with the application requirements.

  • ActiveRecord::ImmutableRelation: Raised when a relation cannot be mutated because it’s already loaded.

Reference: Rails 4.0 active record errors opens a new window

# In Rails 4
class Task < ActiveRecord::Base
end

relation = Task.all
relation.loaded? # => true

# Methods which try to mutate a loaded relation fail.
relation.where!(title: 'TODO')  # => ActiveRecord::ImmutableRelation
relation.limit!(5)              # => ActiveRecord::ImmutableRelation
  • try will not raise an error and a new try! method

Rails core extensions override the Object#try method. This override changed between Rails 3.2 and 4.0: try will now return nil instead of raising a NoMethodError if the receiving object does not respond to the desired method.

You can still get the old behavior by using the new Object#try! method.

# before:
irb(main):012:0>  User.first.try(:age)
#  User Load (1.6ms)  SELECT "users".* FROM "users" LIMIT 1
# NoMethodError: undefined method 'age' for #<User:0x00007f8c12260908>

# after:
irb(main):023:0> User.first.try(:age)
#  User Load (0.3ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
# => nil

irb(main):012:0>  User.first.try!(:age)
#  User Load (1.6ms)  SELECT "users".* FROM "users" LIMIT 1
# NoMethodError: undefined method 'age' for #<User:0x00007f8c12260908>

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'

c. Active Support

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

d. Router

  • Rails 4 will now raise an ActionController::UrlGenerationError for routing issues.

  • 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'

6. Tests

From Ruby 1.9.x onwards, you have to include the test-unit gem opens a new window in your Gemfile if you are using this runner, as it was removed from the standard lib.

  • The way we pass header in Rails 3 and Rails 4 test case is different
# before:
  request.env.merge!(headers)

# after:
  request.headers.merge!(headers)

7. Miscellaneous

  • Rails 4.0 dropped support for plugins

You’ll need to replace them with gems, either by searching for the project on RubyGems opens a new window /Github opens a new window , 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 opens a new window , and feel free to tell us how your upgrade went.

If you’re not on Rails 4.0 yet, we can help! Just contact us! opens a new window .

Get the book