Today will have a rather long and technical post, focused on all the steps needed for migrating big applications from Rails 2 to Rails 3.
First question: why bother?
Rails 2 is great, but Rails 3 is awesome.
Apart from all new goodies given by Rails 3 (assets, increased security, bundler…), more and more Rails libraries ship only for Rails 3.
Not upgrading is no longer an option for an application that is bound to be maintained in the long run.
I got a bunch of Rails 2 applications, and when reading on-line resources, I got strong confidence: upgrading from Rails 2 to 3 is easy! Right?
Well, considering your Rails 2 application kept the simplicity of the default Rails’ blog example, yes: it will be easy.
For real world applications, situation is a bit more complex.
So this post tries to enumerate all the steps I had to take, and all the problems I got when migrating big Rails applications from 2.3.x to 3.2.9.
If not already the case, make sure your application is currently running using the latest version of Rails 2.3.x. If not, migrate first to this version.
Then there are some steps you can perform while your application is still using Rails2: details here. This will greatly simplify your migration afterwards.
- First steps: Rails 3 and upgrade setup
- Upgrade basics
- Check points to upgrade
- Generate Rails3 files
- Merge files with Rails2 backups
- Session secret setting
- Cookie verifier secret
- Routing
- Bundler used to handle dependencies
- Test helpers path
- Application configuration
- filter_parameter_logging deprecated
- Block helpers in ERB
- Deprecated RAILS_ENV, RAILS_ROOT, RAILS_DEFAULT_LOGGER
- Remove useless migration plugin
- Remove useless
.rails2
files
- Other tasks, not as straightforward
- Use raw to avoid escaping HTML characters in views
- Use assets to serve your public images, stylesheets, javascript files
- No more stylesheets controllers to generate CSS files
- Writing helpers using
concat
does not work anymore - Changing table names for models has changed
- Recognizing routes has changed
- Rails3 declare more MIME types
- Accessing current controller from the environment has changed
- Migrate to Devise gem version >= 2.0
- Use Prototype library
- link_to_function API changed and is deprecated
- Models’ attributes can’t be mass assigned anymore by default
- Form error messages don’t work anymore
- The Mailer interface has changed
First steps: Rails 3 and upgrade setup
First thing to do is to install Rails 3.
> gem install rails
Rails developers have provided a little helper script that will diagnose your Rails 2 application and tell you what files need attention.
This script is installed this way:
> ruby script/plugin install git://github.com/rails/rails_upgrade.git Initialized empty Git repository in /path/to/railsapp/vendor/plugins/rails_upgrade/.git/ remote: Counting objects: 27, done. remote: Compressing objects: 100% (22/22), done. remote: Total 27 (delta 3), reused 20 (delta 3) Unpacking objects: 100% (27/27), done. From git://github.com/rails/rails_upgrade * branch HEAD -> FETCH_HEAD Thanks for installing the Rails upgrade plugin. This is a set of generators and analysis tools to help you upgrade your application to Rails 3. It consists of three tasks... To get a feel for what you'll need to change to get your app running, run the application analysis: rake rails:upgrade:check This should give you an idea of the manual changes that need to be done, but you'll probably want to upgrade some of those automatically. The fastest way to do this is to run 'rails .', which will simply generate a new app on top of your existing code. But this generation also has the effect of replacing some existing files, some of which you might not want to replace. To back those up, first run: rake rails:upgrade:backup That will backup files you've probably edited that will be replaced in the upgrade; if you finish the upgrade and find that you don't need the old copies, just delete them. Otherwise, copy their contents back into the new files or run one of the following upgraders... Routes upgrader =============== To generate a new routes file from your existing routes file, simply run the following Rake task: rake rails:upgrade:routes This will output a new routes file that you can copy and paste or pipe into a new, Rails 3 compatible config/routes.rb. Gemfile generator ================= Creating a new Gemfile is as simple as running: rake rails:upgrade:gems This task will extract your config.gem calls and generate code you can put into a bundler compatible Gemfile. Configuration generator ======================= Much of the configuration information that lived in environment.rb now belongs in a new file named config/application.rb; use the following task to generate code you can put into config/application.rb from your existing config/environment.rb: rake rails:upgrade:configuration
Now we are ready to use all those goodies to help us upgrading our application.
Upgrade basics
Check points to upgrade
As the upgrade script told us, let’s start with checking what needs immediate attention.
> rake rails:upgrade:check Deprecated session secret setting Previously, session secret was set directly on ActionController::Base; it's now config.secret_token. More information: http://lindsaar.net/2010/4/7/rails_3_session_secret_and_session_store The culprits: - config/initializers/session_store.rb Old router API The router API has totally changed. More information: http://yehudakatz.com/2009/12/26/the-rails-3-router-rack-it-up/ The culprits: - config/routes.rb Deprecated test_help path You now must require 'rails/test_help' not just 'test_help'. More information: http://weblog.rubyonrails.org/2009/9/1/gem-packaging-best-practices The culprits: - test/test_helper.rb Deprecated filter_parameter_logging calls The list of filtered parameters are now stored in /config/application.rb. For example: config.filter_parameters += [:password] More information: http://de.asciicasts.com/episodes/224-controller-in-rails-3 The culprits: - app/controllers/application_controller.rb New file needed: config/application.rb You need to add a config/application.rb. More information: http://omgbloglol.com/post/353978923/the-path-to-rails-3-approaching-the-upgrade The culprits: - config/application.rb Deprecated ERb helper calls Block helpers that use concat (e.g., form_for) should use <%= instead of <%. The current form will continue to work for now, but you will get deprecation warnings since this form will go away in the future. More information: http://weblog.rubyonrails.org/ The culprits: - app/views/activities/_form.html.erb - app/views/user_carts/_checkout_forms.html.erb Deprecated constant(s) Constants like RAILS_ENV, RAILS_ROOT, and RAILS_DEFAULT_LOGGER are now deprecated. More information: http://litanyagainstfear.com/blog/2010/02/03/the-rails-module/ The culprits: - app/controllers/application_controller.rb - app/helpers/application_helper.rb
Wow! This is quite a big list of culprits.
Let’s backup some files before updating them. Here again, a helper script comes in handy.
> rake rails:upgrade:backup * backing up .gitignore to .gitignore.rails2 * backing up app/controllers/application_controller.rb to app/controllers/application_controller.rb.rails2 * backing up app/helpers/application_helper.rb to app/helpers/application_helper.rb.rails2 * backing up config/routes.rb to config/routes.rb.rails2 * backing up config/environment.rb to config/environment.rb.rails2 * backing up config/environments/development.rb to config/environments/development.rb.rails2 * backing up config/environments/production.rb to config/environments/production.rb.rails2 * backing up config/database.yml to config/database.yml.rails2 * backing up doc/README_FOR_APP to doc/README_FOR_APP.rails2 * backing up test/test_helper.rb to test/test_helper.rb.rails2 This is a list of the files analyzed and backed up (if they existed); you will probably not want the generator to replace them since you probably modified them (but now they're safe if you accidentally do!). - .gitignore - app/controllers/application_controller.rb - app/helpers/application_helper.rb - config/routes.rb - config/environment.rb - config/environments/development.rb - config/environments/production.rb - config/environments/staging.rb - config/database.yml - config.ru - doc/README_FOR_APP - test/test_helper.rb
Generate Rails3 files
If you didn’t backup your files previously (using rake rails:upgrade:backup
or other means), be careful when conflicts are found during the new files generation.
Generating Rails3 files is done this way in your application’s directory:
> rails new . exist create README.rdoc conflict Rakefile Overwrite /path/to/railsapp/Rakefile? (enter "h" for help) [Ynaqdh] Y force Rakefile create config.ru ... Using rails (3.2.9) Using sass (3.2.3) Using sass-rails (3.2.5) Using sqlite3 (1.3.6) Using uglifier (1.3.0) Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
Merge files with Rails2 backups
The backup step has created a bunch of .rails2
files everywhere in your application.
You have to review each one of them, and port their content into their corresponding Rails3 migrated file.
For example: port the content of app/controllers/application_controller.rails2.rb
into app/controllers/application_controller.rb
, respecting the structure of the newly generated file.
Part of the merging conflicts is covered in the following sections.
Session secret setting
Use Railsapp::Application.config.session_store
instead of ActionController::Base.session
to store the session store key, and eventually move the secret to an extra file. Details are given here.
# Be sure to restart your server when you modify this file. # Rails 2 - Obsolete # ActionController::Base.session = { :key => ‘mykeysession’, :secret => ‘somereallylongrandomkey’ } # Rails 3 Railsapp::Application.config.session_store :active_record_store, :key => 'mykeysession'
Cookie verifier secret
The cookie verifier secret is usually set in file config/initializers/cookie_verification_secret.rb
. The way to set it has changed:
# Rails2 # ActionController::Base.cookie_verifier_secret = 'A BIG STRING'; # Rails3 ActionController::Base.config.secret_token = 'A BIG STRING';
Routing
Routing format has completely changed, but here comes a little helper script in handy to migrate most of them:
> rake rails:upgrade:routes >config/routes.rails3.rb
Then, you can review your new Routes file, and replace the content of config/routes.rb
with the one of config/routes.rails3.rb
.
Details of these modifications can be found here.
Here is a little example of a migrated routes.rb file:
Railsapp::Application.routes.draw do # Resources resources :my_resources match 'resources/feed' => 'resources#feed', :as => :feed_resources, :format => 'atom' resources :items do member do get :display end end # Statistics match 'statsdisplay' => 'stats#display', :as => :statsdisplay # User carts resources :user_carts do resources :cart_contents end # Users devise_for :users, :controllers => { :registrations => 'registrations' } resources :users do resources :user_event_subscriptions resources :user_carts member do get :edit_external_password get :current_cart end end # Static pages match 'live' => 'home#live', :as => :live root :to => 'home#index' # Default pages match '/:controller(/:action(/:id))' match '*path' => 'home#handle404' end
Bundler used to handle dependencies
If you don’t know yet about bundler, take a look at this. This is the tool Rails3 now uses to handle Gem dependencies.
If you haven’t done it already, migrate your application to bundler: a little helper script does everything:
> rake rails:upgrade:gems >Gemfile
You can review your Gemfile
, and add eventually add missing dependencies that were formerly declared in your config/environment*
files. Here is an example:
source 'https://rubygems.org' gem 'rails', '3.2.6' # Bundle edge Rails instead: # gem 'rails', :git => 'git://github.com/rails/rails.git' gem 'sqlite3' gem 'json' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'prototype-rails' # To use ActiveModel has_secure_password # gem 'bcrypt-ruby', '~> 3.0.0' # To use Jbuilder templates for JSON # gem 'jbuilder' # Use unicorn as the app server # gem 'unicorn' # Deploy with Capistrano # gem 'capistrano' # To use debugger #gem 'ruby-debug19' gem 'mongrel', '1.2.0.pre2' gem 'nokogiri' gem 'mechanize' gem 'warden' gem 'devise' gem 'devise-encryptable' gem 'cancan' gem 'recaptcha', :require => 'recaptcha/rails' gem 'friendly_id' gem 'dynamic_form' gem 'rUtilAnts', '>= 1.0', :require => false gem 'rails-ajax' group :test do # RSpec gem 'rspec' gem 'rspec-rails' # Cucumber gem 'cucumber-rails', :require => false gem 'database_cleaner' gem 'launchy' # Capybara-webkit gem 'capybara-webkit' # Spork gem 'spork-rails' # Guard gem 'guard' gem 'guard-cucumber' gem 'guard-spork' end
Once your Gemfile has been created, you have to run bundle install
to make sure all dependencies are met.
Test helpers path
Require rails/test_help
instead of test_help
(usually found in test/test_helper.rb
).
ENV["Rails.env"] = "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order. # # Note: You'll currently still have to declare fixtures explicitly in integration tests # -- they do not yet inherit this setting fixtures :all # Add more helper methods to be used by all tests here... end
Application configuration
Most of the application configuration has been moved from config/environment.rb
to a new file called config/application.rb
. A little helper script helps us generate this file:
> rake rails:upgrade:configuration >config/application.rb
Review the configuration file config/application.rb
. Here is an example:
require File.expand_path('../boot', __FILE__) require 'rails/all' 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 module Railsapp class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. # config.autoload_paths += %W(#{config.root}/extras) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [ :password, :password_clear, :password_verify ] # Use SQL instead of Active Record's schema dumper when creating the database. # This is necessary if your schema can't be completely dumped by the schema dumper, # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql # Enforce whitelist mode for mass assignment. # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. # config.active_record.whitelist_attributes = true # Enable the asset pipeline config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' end end
filter_parameter_logging deprecated
Parameters filtered from log files are now defined in the previously generated config/application.rb
file.
Delete all filter_parameter_logging
calls (usually found in app/controllers/application_controller.rb
), and update your config/application.rb
file accordingly.
Block helpers in ERB
This is one of the most annoying changes in big projects: in ERB views, block helpers now return strings instead of silently concatenating their result to the output.
This means that using form_for
, form_tag
, fields_for
and link_to
(even in its block form) are now called using <%=
instead of <%
.
<%= form_for(resource) do |f| %> <p> <%= f.label :name %><br /> <%= f.text_field :name %> </p> <p> <%= f.submit button_name %> </p> <% end %> <%= link_to resource_path do %> <img src="myimg.png"/> <% end %>
You have to replace all former calls in all your views.
Deprecated RAILS_ENV, RAILS_ROOT, RAILS_DEFAULT_LOGGER
This one can be seen as simply as a little text replacement in your source files (details here):
Replace | with: |
---|---|
RAILS_ENV | Rails.env |
RAILS_ROOT | Rails.root |
RAILS_DEFAULT_LOGGER | Rails.logger |
Remove new_rails_default
The file config/initializers/new_rails_default
was intended for Rails2 to Rails3 migration preparation. Every configuration it contains is now a default in Rails3.
You can simply delete this file.
Remove useless migration plugin
The migration plugin installed at the beginning of this tutorial is inside vendor/plugins/rails_upgrade
. However this location is marked as deprecated for plugins, and this directory can be removed, now that we won’t use rake rails:upgrade:*
tasks anymore.
Remove useless .rails2
files
At that point, you have completed all the common migration tasks. For simple websites, that will be all.
You can now remove all .rails2
files.
Next lie extra steps that bigger websites might need to finalize their migration correctly.
Other tasks, not as straightforward
And now comes the candy: all the tasks that were not listed as part of a Rails3 migration.
Some of them might not apply to your application, as it can deal with external gems, or specific usages of the Rails API.
Use raw to avoid escaping HTML characters in views
For big projects using heavy HTML generation in views, this can be a real pain.
In Rails2, you could use view helpers with HTML characters. For example, to display an image as a link:
<%= link_to '<img src="myimg.png"/>', resource_path %>
However in Rails3 for security reasons, this will escape all HTML characters, and instead of having a link on your image, you will have a link on the text <img src="myimg.png"/>
.
If you want to force keeping your string as you intended, use raw
method:
<%= link_to raw('<img src="myimg.png"/>'), resource_path %>
The same goes for any Ruby string that is returned directly into a view:
<%= raw '<p>I don\'t want the p tag to be escaped, therefore I use raw.</p>' %>
Therefore the same goes for any view helper you might have defined that was bound to return HTML snippets:
def add_bold(text) bolded_text = "<b>#{text}</b>" # Rails2 # return bolded_text # Rails3 return raw bolded_text end
Use assets to serve your public images, stylesheets, javascript files
Assets are really awesome, and you should definitely use them.
More info on assets can be found here.
Although Rails3 can continue to deal with all the files stored in the public/
folder the same way Rails2 did, I strongly recommend migrating those files to the assets pipeline.
However when doing so in production mode, Rails3 alters the name of your files served as assets. When you access them from your code, you cannot hard-code them anymore ; instead you will use the asset_path
helper.
# Rails2 mycss_url = "#{ENV['RAILS_RELATIVE_URL_ROOT']}/css/mystylesheet.css" # Rails3 mycss_url = asset_path('mystylesheet.css')
Remember to check that your assets are enabled in file config/application.rb
:
# Enable the asset pipeline config.assets.enabled = true # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0'
No more stylesheets controllers to generate CSS files
When your application needs CSS generation, a common trick in Rails2 was to define a stylesheets controller, and make it render a CSS file like a view.
With Rails3 you don’t need that anymore: CSS files are served using assets, and this mechanism allows CSS files to be generated using lots of different engines: SASS, LESS, ERB…
Writing helpers using concat
does not work anymore
If you have written view helpers that concatenate data to the output (the same way form_for
did in Rails2), you certainly have used the concat
methods.
In Rails3 you will have to change this behavior: your helper methods have to return the HTML strings, and not bother anymore about concatenating them to the output.
# Encapsulate an inner block into a section div def section(&iBlock) inner_html = capture(&iBlock) # Rails2 # concat("<div class=\"section\">#{inner_html}</div>") # Rails3 return raw "<div class=\"section\">#{inner_html}</div>" end
And this change is reflected in your views that now get the string directly from the call, using <%= section do
instead of <% section do
:
<%= section do %> <p>My section content</p> <% end %>
Changing table names for models has changed
If you wanted your model to use a specific table name in the database, you were using set_table_name
. It has been replaced with self.table_name =
.
class MyModel < ActiveRecord::Base # Don't use the default table name # Rails2 # set_table_name 'my_table_for_model' # Rails3 self.table_name = 'my_table_for_model' end
Recognizing routes has changed
In Rails2, when you wanted to recognize a path (ie. translating a URL into the hash giving controller, action), you could use ActionController::Routing::Routes.recognize_path
.
In Rails3, the same call is Rails.application.routes.recognize_path
.
# Rails2 # url_params = ActionController::Routing::Routes.recognize_path('/path/to/resource/15/edit') # Rails3 url_params = Rails.application.routes.recognize_path('/path/to/resource/15/edit')
Rails3 declare more MIME types
By default, Rails2 was missing some important MIME types to be declared. You had to do it by yourself.
In Rails3, common MIME types are already declared by default.
If you were declaring MIME types by yourself, you can comment out the following ones as they have been added into Rails3:
# Rails3: Already declared MIME types #Mime::Type.register 'image/x-windows-bmp', :bmp #Mime::Type.register 'image/gif', :gif #Mime::Type.register 'image/jpeg', :jpeg #Mime::Type.register 'video/mpeg', :mpeg #Mime::Type.register 'application/pdf', :pdf #Mime::Type.register 'image/png', :png #Mime::Type.register 'image/x-tiff', :tiff #Mime::Type.register 'multipart/x-zip', :zip
Accessing current controller from the environment has changed
Some plugins require that your initializers define callbacks that will need access to the current controller. This is ugly, but unfortunately sometimes there is no choice.
In Rails2, this could be done using env['action_controller.rescue.response'].template.controller
.
In Rails3, you have to use env['action_controller.instance']
.
Migrate to Devise gem version >= 2.0
If you were using the Devise gem, you will have to migrate it to a version >= 2.0.
This can be quite painful, all the more so as you customized it deeply.
The best way to do it is to generate from scratch new controllers and views from Devise’ generators after having updated it. More information can be found here.
Basically, here are the points to consider when migrating Devise:
:http_authenticatable
and:activatable
modules have been removed.- Views files have been reorganized, in a
app/views/devise/
directory (devise_mailer/
,confirmations/
,passwords/
,registrations/
,sessions/
,shared/
andunlocks/
directories have been moved there). - Locales have been slightly changed.
- Signing out users is done using
:delete
HTTP method instead of:get
. - Users updating their email does not change them in the database, but set a
unconfirmed_email
column instead. - Some changes have to be done on the database. Here is a migration file I used for this purpose. Don’t forget to adapt it to your own needs:
class MigrateDeviseTo204 < ActiveRecord::Migration def self.up change_table(:users) do |t| t.datetime :reset_password_sent_at t.string :unconfirmed_email # Only if using reconfirmable end remove_column :users, :remember_token end def self.down change_table(:users) do |t| t.string :remember_token end remove_column :users, :reset_password_sent_at remove_column :users, :unconfirmed_email end end
- If you were using cancan with Devise, you may have to change your
app/models/ability.rb
file, and adddevise/
to any Devise action. Here is an example:# User and sessions can [:new, :create, :edit, :update], 'devise/passwords'.to_sym can [:new, :create, :show], 'devise/confirmations'.to_sym can [:new, :create], [ :registrations, 'devise/sessions'.to_sym, User ] can :destroy, 'devise/sessions'.to_sym
Use Prototype library
By default, Rails2 was packaged with Prototype library.
Rails3 ships with jQuery by default. However it is very easy to add Prototype back if you need it in your application.
First, add the prototype dependency in your Gemfile
:
gem 'jquery-rails' gem 'prototype-rails'
Run bundle install
to make sure it is installed.
Then add the prototype inclusions in your asset pipeline:
//= require jquery //= require jquery_ujs //= require prototype //= require_tree .
link_to_function API changed and is deprecated
Method link_to_function
from JavaScriptHelper now takes 2 mandatory arguments instead of 1, and does not accept blocks anymore.
The solution is to not use it anymore, as it is going to be deprecated.
Here is a usage example:
# Rails 2 link_to_function('link text') do |page| page << 'alert(\'Hello\');' end # Rails 3 content_tag(:a, 'link text', :href => '#', : onclick => 'alert(\'Hello\'); return false;')
Models’ attributes can’t be mass assigned anymore by default
When your models are being saved using update
in your controllers, all the attributes given in the hash will be updated.
In Rails3 this is not possible anymore for security reasons: you have to declare in your Models which attributes can be updated, using the attr_accessible
method:
class User < ActiveRecord::Base # Setup accessible (or protected) attributes for your model attr_accessible :email, :name end
Form error messages don’t work anymore
In Rails2, you could call the method error_messages
in your forms to display error messages. This method does not exist anymore.
Simplest way is to add it back, which can be done using the dynamic_form
gem.
# ... gem 'dynamic_form' # ...
The Mailer interface has changed
Here are the differences:
- Mailers are defined in the
app/mailers
directory instead ofapp/models
. - Variables accessible in Mailer views are directly set using
@my_variable
instead of@body['my_variable']
. - Sending mails is done using
MyMailer.my_mail_method(my_args).deliver
instead ofMyMailer.deliver_my_mail_method(my_args)
.
That’s all for what I have found so far.
I will update this list as I find more migration steps.
1 comment