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 .
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.
- Preparations
- Ruby version
- Gems
- Config files (config/)
- Application code
- Tests
- Miscellaneous
- 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.
- Your test suite should have at least 80% code coverage . If you don’t, you should have a dedicated QA team with complete testing scripts.
- 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.
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 to find out.
3. Gems
You can use next_rails ’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 for a similar output.
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, 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 , 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 article by Aaron Patterson explaining this change and its consequences: config.threadsafe!: What does it do?
- 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 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 )
- The
:finder_sql
association option is deprecated in favor of scopes
Note: As of 4.1, :finder_sql
is completely REMOVED from Rails
# 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
- 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
).
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
:
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 , 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
tovalidates_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
, orbelongs_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
# 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
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 ofModel.all
:
# before:
User.scoped
# after:
User.all
- DEPRECATION WARNING: The
:distinct
option forRelation#count
is deprecated. Please useRelation#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
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
# 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 newtry!
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
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 /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! Just contact us! .