Brighton Classified
Brighton Classifieds is an extremely high specification system with some of the most advanced features ever seen in a community based classifieds site.
Many to many mappings with Ruby on Rails
Many to many relationships in Ruby on Rails are easy! In this article we'll look at how to create a relationship between two models, and how to create a simple admin interface to administer the mapping.
For the purposes of this article I'm going to be looking at a simple youtube style application. The application has episodes and I want to do some merchandising, so I'm going to add products to episodes and hopefully make some sales. I want the products to be relevant so I'm going to need the ability to create lots of products and add each of them to one or more episodes. I need a many to many mapping!
I'm assuming you have already created your models so the first thing to do is to create a mapping table like this:
This should have created a migration file with a name something like: db/migrations/013_episode_product_mapping. The number will obviously vary depending on how much development you've already done on your application.
Edit the migration to look something like this:
class CreateEpisodesProductsMapping < ActiveRecord::Migration
def self.up
create_table :episodes_products, :id => false do |t|
t.column :episode_id, :integer
t.column :product_id, :integer
end
end
def self.down
drop_table :episodes_products
end
end
There are a few things to note here:
- Firstly the table is named after the pluralised form of the two models we want to join.
- The model names are alphabetised, ie episodes_products will work but products_episodes will not.
- The table contains exactly two columns named {modelname}_id.
- The rows don't have id's of their own as this table doesn't define any objects of it's own. :id => false prevents rails from creating the default id column. If you forget this you'll get duplicate id errors when you create your second mapping.
Now we need to tell the models to use the mapping table so edit your product.rb model file like this:
class Product < ActiveRecord::Base
has_and_belongs_to_many :episodes
end
and your episode.rb model file like this:
class Episode < ActiveRecord::Base
has_and_belongs_to_many :products
end
That's the model and database dealt with, now we need to modify the controller. For this example I'm going to edit the product controller because I want to be able to select episodes from a list when I create a product. You might want to make this more symmetrical.
So, in the product_controller create variables to hold your a list of your episodes like so:
def new
@product = Product.new
@episodes = Episode.find(:all)
end
def edit
@product = Product.find(params[:id])
@episodes = Episode.find(:all)
end
We'll use these in our view.
Now edit product/new.rhtml to add a list of episodes. The list should look something like this:
<h2>Episodes</h2>
<% if @episodes.length == 0 %>
<p>There are no episodes</p>
<% else %>
<ul>
<% for e in @episodes do -%>
<li>
<input type="checkbox" name="episode_ids[]" id="<%= e.id %>" value="<%= e.id %>"<% if @product.episodes.include? e %> checked="checked"<%end%>>
<label for="<%= e.id %>"><%= e.title %></label>
</li>
<% end %>
</ul>
You'll also need to do something similar for your product/edit.rhtml file. Note the name attribute of the checkbox. This can be anything you like as we'll refer to it directly later, just as long as it contains the string'[]' which tells rails to generate an array.
Finally we'll modify the product_controller once more to put these episodes into the product.episodes array. So edit the create and update methods like so:
def create
@product = Product.new(params[:product])
@episodes = Episode.find(:all)
@product.episodes = Episode.find(@params[:episode_ids]) if @params[:episode_ids]
if @product.save
flash[:notice] = 'Product was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
def update
@product = Product.find(params[:id])
@episodes = Episode.find(:all)
@product.episodes = Episode.find(@params[:episode_ids]) if @params[:episode_ids]
if @product.update_attributes(params[:product])
flash[:notice] = 'Product was successfully updated.'
redirect_to :action => 'show', :id => @product
else
render :action => 'edit'
end
end
note the line which generates the @product.episodes array. We're passing ActiveRecord an array of numbers from the form submission and getting back an array of Episodes which we then assign to the @product.episodes attribute. Calling @product.save updates the mapping table creating the mapping between the two objects and so your many to many mapping is complete.
One issue to look out for. When creating the mapping, rails revalidates the episodes as well as the new product. If your app is under development it's possible your older model objects may no longer validate, eg your episodes may have required fields which were not previously required. If this is the case, rails will throw a rather cryptic 'episodes is not valid' error on saving the product. To fix this make sure all your objects still pass validation.