Tuesday, 29 May 2012

Moving to Rails 3 (finally)

I have been putting off moving to Rails 3 for many months (years?) because I thought it would be a nightmare. Finally I have bitten the bullet... and it was a nightmare. This post gives a brief view of what I did and what I found on the way just in case there is anyone out there still planning to do this.

Bear in mind that this project is a few years old now, and some of the problems I had may be becvause I am still doing things the very old way.

Preparation

Make sure you have a ton of tests so that at each stage you can be reasonable confident your system is still working.

Stage 1 - Upgrading To Rails 2.3!

Start by upgrading your project as much as possible but still as a Rails 2.3 project. The first thing to do is to get to the most recent version of Rails 2.3 (2.3.14 as far as I know). Also, make sure you are using Ruby 1.9.2+, and a recent JRuby (1.6.7 for me); check that works okay.

Integrate your libraries into the project (and update requires as needed):
/lib/myplugin rather than vendors/plugins/myplugin/lib
/config/initializers/myplugin.rb rather than vendors/plugins/myplugin/init.rb
/test/units/lib/myplugin rather than vendors/plugins/myplugin/test

Extension to ActionView::Helpers all need to go into application_helper.rb

For other people's libraries, let Bundler do it (I think), so now we move to Bundler for handling gems:
http://gembundler.com/rails23.html

Check your gems are up-to-date; not only you are using the most recent version, but that a new version has been released in the last year or so that will be compatible with Rails 3 (I was using file_column, which has not been updated for used, so changed over to Paperclip). Try to avoid plugins in favour of gems entirely.
That said, do not use activerecord-jdbc-adapter 1.2.2, stick with 1.2.1 or 1.2.0 (see http://ruby.11.n6.nabble.com/saved-model-object-ID-always-1-with-jruby-rack-td4467855.html).

Make sure all your partials have the .html.erb extension, rather than .rhtml.

Other suggested changes here:
http://www.simonecarletti.com/blog/2010/07/the-way-to-rails-3/

Rails 3 uses a slightly different set of rules for plurals, and now would be the best time to update your system to reflect that. I had already got to the next stage, so then had to go back. What you could try is copying the rules from Rails 3 into your own project and checking that it still works. The rule that messed me up was data/datum. Make sure you have a unit test that checks the plural and singular both work in the Rails 3 way, and that all your tests work with the new rules in place.

So now my project works fine of Rails 2.3, but is set up to ease the transition (in theory).



Stage 2 - Create a Dummy Rails 3 Project

I found that Rails 3 will not just download the gems it needs. I guess this is Bundler, and it may be due to my system, though this was necessary on two computers, one using Netbeans, the other from the command line. These are the gems I had to install by hand (some being updates to gems already installed).

gem install rails
gem install rake -v '0.9.2.2'
gem install multi_json -v '1.1.0'
gem install activerecord-jdbc-adapter -v '1.2.2'
gem install jdbc-postgres -v '9.1.901'
gem install activerecord-jdbcpostgresql-adapter -v '1.2.2'
gem install coffee-script-source -v '1.2.0'
gem install execjs -v '1.3.0'
gem install coffee-script -v '2.2.0'
gem install coffee-rails -v '3.2.2'
gem install jquery-rails -v '2.0.1'
gem install jruby-openssl -v '0.7.6.1'
gem install sass -v '3.1.15'
gem install sass-rails -v '3.2.4'
gem install therubyrhino -v '1.73.1'
gem install uglifier -v '1.2.3'

Now I could create a new project. I did this inside NetBeans; Netbeans 6.9 could not create a new project from existing files with Rails 3. Behind the scenes, NetBeans did something like this:

jruby -S rails new myapp -m=http://jruby.org/rails3.rb -d=jdbcpostgresql

I copied the database.yml file across from the original project, generated a test scafford (for a table called "post"), migrated the database, and fired up the web server (Webrick) to check that it would start up. At this point I am confident that Rails 3 works on my system.


Stage 3 - Rails 3

Copy across the text inside Gemfile
Just the part that has all your gems.
Run "bundle install"
Run "rake test".

Copy across your library files
Run "rake test".

Copy across other files from config/initializers
Run "rake test" (at this point I discovered that an extention to Array (to_hash) was screwing up rake; I went back to my Rails 2 project, updated that, checked tests ran okay on that, then copied across again).

Copy across your migrations
At this point Rails thinks you have one table in your database, "posts", but a bunch of migrations with earlier timestamps. To get everything synchronised, I deleted db/schema.rb, and cleared the development database completely from the database admin software. Then I could use rake to migrate the database from scratch (which itself turned up a couple of issues).
Run rake "db:test:prepare", then "rake test".

Copy across models
There are a number of changes required here.
1. Any named_scope become scope
2. Validations also have changed. Instead of being type orientated (validates_presence_of with a bunch of fields) it is not field base (validates with a bunch of constraints). The change makes sense, but is a pain if you have a project of any size.
http://guides.rubyonrails.org/active_record_validations_callbacks.html
3. Also, anywhere that you save a model without validating it, you now need to change
save(false)
to
save(:validate => false)
4. I found that having my models in subfolders meant that I had to namespace them. Various posts on the web indicate this should not be necessary (and it certainly was not for Rails 2.3), but I could not get it to work without namespaces. It was a serious PITA to namespace two dozen models!

Copy across public files
Rails 3 is designed to allow several projects to run at the same time, so by default will look for these files in your application folder, specifically app/assets, so copy them into there (alternatively, set config.assets.enabled = false in config/application.rb). Although they go in subfolders, they are references without the subfolder, so app\assets\stylesheets\main.css is at URL assets\main.css; this may mean you need to update your stylesheet to properly reference any images (though this is not neater).

Copy across your layout
I use a single layout for the whole project and it had links for logging in, so this was not straightforward...

Copy across test/test_helper
Change
require 'test_help'
to
require 'rails/test_help'

Do not attempt to use self.use_transactional_fixtures; it does not seem to work wth Rails 3 (not for me, anyway). I also copied across some data files that were used to populate tables for testing at this point.
Test your units.

Copy across unit tests
Should not need any changes, unless you are now using namespaces for your models.
Test your units

Copy across helpers
Rails 3 seems to load up helpers before models, while previous versions did the reverse. This means that helpers that refer to constants and methods in your models are screwed...
Run "rake test"

Copy across controllers, views and functional tests
The namespace issue was a problem for me here again.





Routes

Routes are set up differently:
  map.namespace :drum_log do |submap|
submap.resources :drum_meta_data
submap.resources :drums,
:collection => { :find => :get,
:list => :get,
:keg_print_label => :get,
:kegs => :get,
:blank_print_label => :get,
:blank => :get,
}
end

Becomes

  namespace :drum_log do
resources :drum_meta_data
resources :drums do
collection do
get :find
get :list
get :kegs
get :blank
end
end
end



Rails 3 is a bit more fussy with its URLs, so you have to have users/23/enable, not users/enable23, for example. Also, it relies on PUT, POST, GET and DELETE, so post/45/edit should now be GET post/45. For the most part this is all done behind the scenes, but I did have to update my integration tests for these new rules.

Named routes seem to take precedent, so when testing when a page is redirected to, give the named route, not the controller/action/id
#assert_redirected_to :controller => 'sessions', :action => 'new'
assert_redirected_to '/login'



Other Issues

Rails now has the correct inflection for datum/data - great, but a real pain to discover, and to change.


Rails 3 converts Time objects to Date fields in a database slightly differently, so that times around midnight may be counted as the day before. I did not explore this fully, but my guess is that it now uses UCT, and a time on a certain day in one timezone may be a different day in another timezone.


request.request_uri no long works.


Previously Rails used the h() method to escape test (so "bold" would become "&ltb>bold&lt/b>"). In Rails 3 escaping is done by default, and if you want the raw string you need to flag is as HTML safe (like this "bold".html_safe).


error_messages_for
http://www.suffix.be/blog/error-messages-for-rails3


ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes
http://www.h-online.com/security/news/item/Rails-3-2-3-makes-mass-assignment-change-1498547.html


ERB blocks now require an equal sign for the content to be shown (makes sense, but took a while for me to realise why nothing was appearing - and not going to be caught by any functional tests).
<%= form_tag user_log_sessions_path do %>

<%= submit_tag 'Log in' %>


<% end %>


Rails 2 had no problem sending an object to set up an association in an initializing hash; Rails 3 seems to demand the id of the object.

class SampleLog::Sample < ActiveRecord::Base
belongs_to :batch
end

# Sample.new :batch => my_batch
Sample.new :batch_id => my_batch.id


I had to change my partials testing significantly following this:
http://bibwild.wordpress.com/2012/05/23/rails-3-x-testunit-testing-view-partials/
Also, testing helpers can be done by extending ActionView::TestCase now.


The content of an uploaded file is now accessed though its tempfile attribute.
#ary = params[:file].readlines
ary = params[:file].tempfile.readlines


To test file uploads, use Rack::Test::UploadedFile
upload = Rack::Test::UploadedFile.new("#{Rails.root}/#{TEST_FILES_DIR}/preSief.xml", "text/xml")
post :iuclid_upload, :type => 'sief', :file => upload

You need to expose tempfile in Rack::Test::UploadedFile to get this to work.
You need to expose tempfile in Rack::Test::UploadedFile to get this to work.
module Rack
module Test
class UploadedFile
def tempfile
@tempfile
end
end
end
end



If a database query takes longer than half a second, Rails will access the "explain" method of your database adapter. Great. However, if your database adapter does not have an "explain" method, you see a "undefined method `explain' " page which gives you no clue about why it is happening. Hitting reload will often solve this (I guess your earlier request is cached), but you can change the threshold, or modify your code so database access is faster. Modify this line in config/environments/development.rb.
config.active_record.auto_explain_threshold_in_seconds = 0.5

http://guides.rubyonrails.org/3_2_release_notes.html#automatic-query-explains


The Rails mailer is rather different now.
http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-layouts


Rails builds links rather differently in url_for (and so in link_to, etc.). If you use namespaces, you shouldsend it an array, rather than hash.
{ :controller => '/sub_folder/posts', :action => 'statistics' }
... becomes...
[:statistics, :sub_folder, :posts]
Behind the scenes the array is used to build a method (called statistics_sub_folder_posts_path in this case), which has to match a path listed when you do rake:routes. A lot of the changes to Rails three are annoying because of the effort to convert to them, but I can see the logic. This one I prefered the old way. By the way, the old method works for links within the namespace, or if you are not using namespaces.


Deploying
I would recommend running a test deployment, as I ran into all sorts of problems.
Make sure you are running Ruby 1.9, not 1.8. Change warble.rb appropriately if using Warbler.
Ensure you have the most recent jruby gem installed.
Precompile your assets if packaging ("rake assets:precompile").