Learning Rails: Many-to-Many Relationships

To anyone who finds this tutorial useful I’d love some feedback or example implementations. Just leave a note at the bottom :)

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 < ActiveRecord::Base
  has_many  :taggings,:dependent => true
  has_many  :tags, :through => :taggings, :uniq => true

  has_many  :imagesets,:dependent => true
  has_many  :sets, :through => :imagesets, :uniq => true
end
class Set < ActiveRecord::Base
  has_many  :imagesets,:dependent => true
  has_many  :images, :through => :imagesets, :uniq => true

  has_many  :setcollections, :dependent => true
  has_many  :collections, :through => :setcollections, :uniq => true
end
class Collection < ActiveRecord::Base
  has_many  :setcollections, :dependent => true
  has_many  :collections, :through => :setcollections, :uniq => true
end
class Tagging < ActiveRecord::Base
  belongs_to  :image
  belongs_to  :tag
end
class Imageset < ActiveRecord::Base
  belongs_to    :image
  belongs_to    :set
  acts_as_list  :scope => :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 < ActiveRecord::Base
  has_many  :taggings,:dependent => true
  has_many  :tags, :through => :taggings, :uniq => true

  has_many  :imagesets,:dependent => true
  has_many  :sets, :through => :imagesets, :uniq => true
end
class Set < ActiveRecord::Base
  has_many  :imagesets,:dependent => 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 < ActiveRecord::Base
  belongs_to    :image
  belongs_to    :set
  acts_as_list  :scope => :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.

<ul>
< % for image in @images %>
  <li>
    < %= check_box_tag "set[image_ids][]", image.id, @set.images.include?(image) %>
    
  </li>
< % end %>
</ul>

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.

Update

Andrew was asking about the schema for the database tables, so I’ll add in the migrate details.

@Andrew: I will be writing up anything I find useful that I can’t find assistance with on the web.

In terms of a migration for the tables, it would look something like this (I’ll update in the post too):

class CreateImages < ActiveRecord::Migration
  def self.up
    create_table :images do |t|
      t.column :name, :string, :null => false
      t.column :permalink, :string, :null => false
      t.column :description, :text, :null => false
      t.column :filename, :text, :null => false
    end
  end

  def self.down
    drop_table :images
  end
end
class CreateSets < ActiveRecord::Migration
  def self.up
    create_table :sets do |t|
      t.column :name, :string, :null => false
      t.column :permalink, :string, :null => false
      t.column :description, :text, :default => nil
    end
  end

  def self.down
    drop_table :sets
  end
end

and the complex join table

class CreateImagesets < ActiveRecord::Migration
  def self.up
    create_table :imagesets do |t|
      t.column :position, :int, :null => false
      t.column :gallery_id, :int, :null => false
      t.column :image_id, :int, :null => false
    end

    execute "alter table imagesets add foreign key (set_id) references sets(id)"
    execute "alter table imagesets add foreign key (image_id) references images(id)"

  end

  def self.down
    drop_table :imagesets
  end
end

9 Responses to “Learning Rails: Many-to-Many Relationships”


  1. 1 Fradam

    …due to a background in PHP, with it’s rather lax type strictness….

    Typo? I suspect you meant ‘almost non-existent’ ;)

  2. 2 Andrew

    Great tutorial… there should be more stuff like this out there.

    One question: How would you set up the tables for this?

    Thanks.

  3. 3 Joel

    @Andrew: I will be writing up anything I find useful that I can’t find assistance with on the web.

    In terms of a migration for the tables, it would look something like this (I’ll update in the post too):

    class CreateImages < ActiveRecord::Migration
      def self.up
        create_table :images do |t|
          t.column :name, :string, :null => false
          t.column :permalink, :string, :null => false
          t.column :description, :text, :null => false
          t.column :filename, :text, :null => false
        end
      end
    
      def self.down
        drop_table :images
      end
    end
    class CreateSets < ActiveRecord::Migration
      def self.up
        create_table :sets do |t|
          t.column :name, :string, :null => false
          t.column :permalink, :string, :null => false
          t.column :description, :text, :default => nil
        end
      end
    
      def self.down
        drop_table :sets
      end
    end

    and the complex join table

    class CreateImagesets < ActiveRecord::Migration
      def self.up
        create_table :imagesets do |t|
          t.column :position, :int, :null => false
          t.column :gallery_id, :int, :null => false
          t.column :image_id, :int, :null => false
        end
    
        execute “alter table imagesets add foreign key (set_id) references sets(id)”
        execute “alter table imagesets add foreign key (image_id) references images(id)”
    
      end
    
      def self.down
        drop_table :imagesets
      end
    end
  4. 4 sarah

    ok i decided to read this post as i thought it would be some heart felt spill on life and woman. PLEASE NEVER do that to me again!!! you are banned from providing misleading titles in you posts!!!

  5. 5 Joel

    You obviously failed to notice where it was filed (Ruby on Rails, design programming) or it’s tags (Technorati , , , , , , , ) and those combined with the title referring to “Learning Rails” makes it pretty obvious that it was not about spilling my heart on life, love and women.

  6. 6 confused

    For the title “Learning Rails: Many-to-Many Relationships”, I don’t think you could have come up with a more confusing example. If you intend for beginners to “Learn” about “Many-to-Many Relationships”, a stripped down example (with 3 models maximum) would be more effective.

    Thank you, none the less, for the code, once I decipher it.

  7. 7 Joel

    @confused: while I understand your comment regarding the complexity of the tutorial, it was because I couldn’t find any tutorials that covered ordered-many-to-many relationships that I wrote this one. True, it does involve two relationships (collection-to-set and set-to-image) and thus has five tables, it was based on a real world (and highly useful) “gallery” system - flickr. So, I apologise for the complexity but countenance that this was about my learning process and the tips and tricks associated with how to do things when I couldn’t find relevant tutorials already in existence.

  8. 8 grateful

    Joel: I disagree with confused and reallyu appreciate the example. Yes, a couple of tables than the basics but it was based on a real world example of Flickr which made this tutorial different, and in my opinion, better. There are a handful of the way-too simplified versions out there for people who want the very basics. Your example had the added advantage of showing a real-world practical dimension that the others don’t due to the simplicity. Thanks!

  9. 9 dwhsix

    Great example for a non-trivial m:m relationship. Wasn’t /exactly/ what I needed, but pointed me in the right direction to resolve a problem that had me baffled for a day…

Leave a Reply