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_railsrequires 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 = truedirective 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_sqlassociation 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: truetovalidates_format_ofwhen 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_toassociations 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
extendassociation 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.scopedis deprecated in favor ofModel.all:
# before:
User.scoped
# after:
User.all
- DEPRECATION WARNING: The
:distinctoption forRelation#countis deprecated. Please useRelation#distinctinstead. (eg.relation.distinct.count).
# before:
Model.where(...).count(distinct: true)
# after:
Model.where(...).distinct.count
increment_open_transactionsis 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
touchmethod 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
trywill 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-observersgem, 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::UrlGenerationErrorfor 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! .