Thursday, May 1, 2008

5 Rails Tips

Here's my entry for the Railscasts 100th Episode Contest - hope you find these tips useful.

1. Create SEO friendly URLs for multi-word resources with edge Rails


Let's say you're creating a web application that documents various aspects of computers. Naturally, you're going to want to be like all the cool kids and make your application RESTful. Let's go ahead and create a resource for operating systems:


$ ruby script/generate resource OperatingSystem

When running the app locally, by default you'll find this resource at localhost:3000/operating_systems. But in order to get the best bang for your SEO buck, you should use hyphens in your URLs (http://www.searchenginejournal.com/google-underscores-hyphens/6010/). The solution for edge Rails is to use the :as option to map.resources in config/routes.rb. First, freeze edge Rails into your app:


$ rake rails:freeze:edge

Then, update config/routes.rb with the following:


map.resources :operating_systems, :as => 'operating-systems'

Voila! Now you can browse to your operating system resource at localhost:3000/operating-systems.



2. ActiveResource != ActiveRecord

The team that has worked on ActiveResource has done a great job of making using it as painless as possible. On the surface, consuming a RESTful service seems very similar to using ActiveRecord with a database. It's easy to assume that everything works the same way. Don't fall into that trap.


Make sure you're aware of what's going on behind the scenes. Tail your logs and take a look at the calls that ActiveResource is making. Let's say you write an app that displays the weather, and you have a Forecast model. At first, your app is built using ActiveRecord. You may have a place in your code that calls Forecast.find(:first). This will generate the following call:


SELECT * FROM forecasts LIMIT 1;

After a while, you get tired of typing in the forecast, so you decide to switch to using a web service that does it for you. The only problem is that the same Forecast.find(:first) will generate the following service call:


GET http://url_for_web_service.com/forecasts

That means you're getting all the forecasts, and then ActiveResource is selecting the first one for you. That could be a big problem if there's 10,000 forecasts.


So keep an eye on your logs and be aware of the subtle differences.



3. Working with collections in views

When iterating through a collection in a view, one underused helper is cycle. cycle takes an array parameter with an optional name parameter. The first example in the official Rails documentation suggests using to alternate CSS classes for tables rows:



# Alternate CSS classes for even and odd numbers...
@items = [1,2,3,4]
<table>
<% @items.each do |item| %>
<tr class="<%= cycle("even", "odd") -%>">
<td>item</td>
</tr>
<% end %>
</table>

Another solution for the same problem (if you have an array of numbers) is to make use of ActiveSupport's odd? and even? helpers:



# Alternate CSS classes for even and odd numbers...
@items = [1,2,3,4]
<table>
<% @items.each do |item| %>
<tr class="<%= item.even ? "even" : "odd" -%>">
<td>item</td>
</tr>
<% end %>
</table>

When I need to keep track of the items in the collection, I most often end up using the each_with_index method from Ruby's Enumerable module:



# Alternate CSS classes for even and odd numbers...
@items = [1,2,3,4]
<table>
<% @items.each_with_index do |item, index| %>
<tr class="<%= index.even? ? "even" : "odd" -%>">
<td>item</td>
</tr>
<% end %>
</table>

This technique has the advantage of giving you the index of each item in the collection. This can come in handy when making Ajax calls - you can give the wrapping element an id that corresponds to the item's index:



# Give each table row an id that uses the item's index in the collection
@items = [1,2,3,4]
<table>
<% @items.each_with_index do |item, index| %>
<tr id="item-<%= index %>">
<td>item</td>
</tr>
<% end %>
</table>

You now have the hook you need if you want to delete that table row with a subsequent Ajax call.



4. Keep your gems local, without Edge Rails

Edge Rails has added the ability to explicitly declare your app's gem dependencies in config/environment.rb with the config.gem syntax. See Ryan Daigle's write-up at http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies for a good description of the new functionality. Freeze Edge Rails into your apps and you'll see the following new Rake commands available:



$ rake -D gem

rake gems
List the gems that this rails application depends on

rake gems:build
Build any native extensions for unpacked gems

rake gems:install
Installs all required gems for this application.

rake gems:unpack
Unpacks the specified gem into vendor/gems.

rake rails:freeze:gems
Lock this application to the current gems (by unpacking them into vendor/rails)

Even if you're not ready to use Edge Rails, you can ensure that your app will always have the gem code it depends on available. It's certainly not as clean, and it does take a little more work. Move into your vendor/plugins directory, and for each gem your app depends on, just do:


$ sudo gem unpack <gem name>

This will unpack the given gem into vendor/plugins and ensure that the gem's code will be loaded when your app starts app. It's a nice way to ensure that your app will have everything it needs when, for example, you deploy on a shared host.



5. Keep a summary of your model schema inside the model


One of the nice things about the DataMapper ORM (http://datamapper.org/) is having the model's schema laid out right in the model code. When working with ActiveRecord models, I sometimes find myself returning to old code and forgetting what the schema was. Naturally, you could jump into script/console and do:


>> ModelName.new.attributes

to see what's available. But that doesn't give you the full schema. The solution is Dave Thomas' annotate_models plugin.


$ ruby script/plugin install http://repo.pragprog.com/svn/Public/plugins/annotate_models/

The plugin adds a new rake task for you to generate the model annotations:


$ rake annotate_models

Here's a sample of what the plugin adds to your model:




# Schema version: 3
#
# Table name: programs
#
# id :integer(11) not null, primary key
# title :string(255)
# description :text
# asset_url :string(255)
# link :string(255)
# language :string(255)
# author :string(255)
#

class Program<ActiveRecord::Base
end

Please note that the annotate_models plugin has been enhanced and released as a gem by Cuong Tran, among others. Source code is available at http://github.com/ctran/annotate_models/tree/master, and installation is as simple as:


$ sudo gem install annotate-models


Okay, I said it would be only five tips, but here's one more: get local documentation.


Just about everywhere you go these days, you can find an Internet connection. But just in case, I like to have both Ruby and Rails documentation on my laptop. Download the Ruby documentation here: http://www.ruby-doc.org/downloads and the Rails documentation here: http://www.railsbrain.com.


That's it for this post. Good luck with your next Rails app!