Dinky Red Panda is the personal site of Stephen Allred

Addressing Models with Paths in Rails 3

Sunday, May 29, 2011

When you first start with Rails, you at some point probably followed the guide on setting up a blog in under 15 minutes. This guide serves as an excellent introduction to Rails, but if you've come from a bloging platform such as Wordpress or Movable Type, you are probably used to human friendly addresses for blog articles. The guide unfortunately omits how to implement this functionality in Rails. While this is a trivial undertaking for even a semi–experienced Rails developer, a beginner might not know where to start. I hope to explain how to add this functionally to a Rails project.

In a basic Rails controller the show action code probably looks something like this:

def show
  @item_inventory = ItemInventory.find(params[:id])
end

Notice the find call, and the value from the params hash it is passed (the id's value). On a standard resourceful route the show action is mapped as follows:

/<model_name>/:id

This means that that if we were to visit /item_inventory/1, for example, the :id value in the params hash would be 1. The find method on a model will find by id. So, in this situation, if an inventory item with the id of 1 exists it will be returned by the find call. To use human readable addresses such as /item_inventory/aer9_laser_rifle we will need to alter the show action.

Before altering the show action, we must add a string to the model which we can match against to find an inventory item. I am calling it path, but you could call it permalink, or slug etc. We will need to write a simple migration to add this property to the database as follows (remember you can create an empty migration using the rails g migration terminal command):

class CreateInventoryItemPathColumn < ActiveRecord::Migration
  def self.up
    add_column :item_inventories, :path, :string
  end

  def self.down
    remove_column :item_inventories, :path, :string
  end
end

As this field is going to be regularly used, it is worth adding a database index to it. This can be done with the following migration:

class InventoryItemAddIndexToPath < ActiveRecord::Migration
  def self.up
    add_index :item_inventories, :path, { :unique => true }
  end

  def self.down
    remove_index :item_inventories, :path
  end
end

We now need to update the show action code to make use of this new property. As mentioned earlier, the find(params[:id]) call finds by id. Now that our value for :id in the params hash is "aer9_laser_rifle" we will want to call a method that will return the inventory item who's path matches that string. As it happens, rails provides find methods automatically for each property on a model. In this case, the one we want is find_by_path() (these methods follow the naming convention of find_by_). To ensure you get a 404 error for an :id string that doesn't match, you should use the "unsafe" version of the method that ends in an exclamation mark (ruby's convention is "safe" methods return nil in an error case, "unsafe" methods throw an exception). So, the correct show action code should be as follows:

def show
    @item_inventory = ItemInventory.find_by_path!(params[:id])
end

Now that we have changed the show method, we will have to update the links to inventory items. To demonstrate I will update the item inventory's index view's code as an example. It is a case of rinse and repeat for any other links in your rails project.

Rather than passing the item_inventory to the item_inventory_path() method in link to, which gives you the path ending in the item_inventory's id, we need to pass item_inventory.path. This will give us the address ending in the item inventory's path. So, the index view's code changes from (no, I don't endorse using HTML tables…):

<h1>Listing item_inventories</h1>

<table>
  <tr>
    <th></th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

  <% @item_inventories.each do |item_inventory| %>
    <tr>
      <td><%= item_inventory.title %></td>
      <td><%= link_to 'Show', item_inventory_path(item_inventory) %></td>
      <td><%= link_to 'Edit', edit_item_inventory_path(item_inventory) %></td>
      <td><%= link_to 'Destroy', item_inventory, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<br />

<%= link_to 'New Item inventory', new_item_inventory_path %>

to:

<h1>Listing item_inventories</h1>

<table>
  <tr>
    <th></th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

  <% @item_inventories.each do |item_inventory| %>
    <tr>
      <td><%= item_inventory.title %></td>
      <td><%= link_to 'Show', item_inventory_path(item_inventory.path) %></td>
      <td><%= link_to 'Edit', edit_item_inventory_path(item_inventory) %></td>
      <td><%= link_to 'Destroy', item_inventory, :confirm => 'Are you sure?', :method => :delete %></td>
    </tr>
  <% end %>
</table>

<br />

<%= link_to 'New Item inventory', new_item_inventory_path %>

That's it! You can find an example project on github, and if you have problems you can email me, message me on github or comment on the code in question in the github repository.

Cheers.