Add something new to Virb:

Virb

Are you sure you want to delete that?

or Cancel

 

Posted on Jun 3, 2007

Learning Rails: Many-to-Many Relationships

So I've had the books in my possession for almost a year now, and I've been picking at it when I've had time and am part way through a "small" project called brewr. As I needed something with a little less degree of difficulty I've decided to develop the backend of jc-photo.net by hand myself and am using the new kid on the block, coding wise, Ruby on Rails. There's plenty of history already written on the group behind it (37Signals). Anyhow, on with the show.


Many to Many Relationships with Details


No, we're not talking about polygamy here, but rather the issues surrounding with linking models together.


Obviously with almost every database model you will be looking to link various bits of disparate data in different tables together. With RoR you have a number of options. There's the has_and_belongs_to_many option, however as a join table this is limited as it gives no option for additional details about that join. Enter has_many :through (and it even has a blog dedicated to it!). Why is this important? Well, imagine you want to replicate flickr's image functionality with tagging, sets and collections.


The Model


Obviously there are a set of relationships. In RoR this could be represented, taking into account a desire to order your sets in collections and images in your sets, by:


class Image  true
has_many :tags, :through => :taggings, :uniq => true

has_many :imagesets,:dependent => true
has_many :sets, :through => :imagesets, :uniq => true
end

class Set  true
has_many :images, :through => :imagesets, :uniq => true

has_many :setcollections, :dependent => true
has_many :collections, :through => :setcollections, :uniq => true
end

class Collection  true
has_many :collections, :through => :setcollections, :uniq => true
end

class Tagging < ActiveRecord::Base
belongs_to :image
belongs_to :tag
end


class Imageset  :sets
end

class Setcollection < ActiveRecord::Base
belongs_to :set
belongs_to :collection
end

If we look at the case of images in a set by itself, for ease of this write up, we need to add a little handler in there (you'll see why in a bit). This is due to the desire to add an ordering component to our models. This would then alter, slightly, the models to this:

class Image  true
has_many :tags, :through => :taggings, :uniq => true

has_many :imagesets,:dependent => true
has_many :sets, :through => :imagesets, :uniq => true
end

class Set  true
has_many :images, :through => :imagesets, :uniq => true

has_many :setcollections, :dependent => true
has_many :collections, :through => :setcollections, :uniq => true

def image_ids=(image_ids)
image_ids.map!(&:to_i)
setcollections.each do |setcollection|
setcollection.destroy unless image_ids.include?setcollection.gallery_id
end
image_ids.each do |image_id|
self.setcollections.create(:image_id => image_id, :position => self.setcollections.length) unless setcollections.any?{ |sc| sc.image_id == image_id }
end
end
end

class Imageset  :sets
end

The Controller


So now we have our relationships set up, we need to be able to, say, tag an image with tags. Let us assume that it is done on the editing phase for ease of an example.


def edit_set
@set = Set.find(params[:id])
@images = Image.find(:all, :order => "name")
end

We grab all the images (though there may be better ways of handling this part in the long term) in addition to the actual set that we are looking at.


def update_set
params[:set][:image_ids] ||= []
@set = Set.find(params[:id])

if @set.update_attributes(params[:set])
@set.save
flash[:notice] = "Set was susccessfully updated."
redirect_to :action => 'view_set', :id => @set
else
render :action => 'edit_set', :id => @set
end
end

The reason why we defined the function image_ids in the Set model is so that we can use the params[:set][:image_ids] to add or delete image references in the join table. When new ones are added, they are added at the end of the acts_as_list list. The catch here is that you need to force the items in the image_ids array to be integers, not strings (as is passed by the html form). This turned out to be a major debug point for me due to a background in PHP, with it's rather lax type strictness.


The View


Now we need only cycle through the images for the check boxes, pre-checking where an existing join occurs (using the third boolean argument of check_box_tag @set.images.include?(image)). The update phase also takes care of the ordered nature of our set, with new images added after existing. You can then use the methodology well described to move the image reference up or down for each gallery.













Conclusion


It took me some time to get to this point, with a major stumbling block being Ruby's more strict method of ensuring variable types are treated differently - namely a string of 1 is not equivelant to an integer 1.


has many, many to many relationship, programming, Rails, Ruby, ruby on rails, through, tutorial

Loading comments...

Likes

Details

Viewed 739 times

© 2007 'ju:femaiz

virb.com/t/65874
tweet!

Flag this text post!

Flag this text post as:

or Cancel

 

Advertisement

Flag this profile!

Flag this profile as:

or Cancel