Posted on Jun 3, 2007
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.
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.
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
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.
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.
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.
Loading comments...