December 10, 2007...4:32 pm

:has_many, :belongs_to, :through, and :include

Jump to Comments

Hi,

I’m sure the following has been discussed 100s of times, and there is probably an obvious answer; either that or its all been fixed in Rails2.0 …

We have a simple relationship between three models, written in shorthand as follows:

Venue :has_many Gig :has_many Image

and

Image :belongs_to Gig :belongs to Venue.

It is easy to answer the question, “What are all the images for all the gigs associated with a venue?”.

Answer: specify “has_many :images, :through => :gigs” in the Venue model (shown below). This gives you the venue.images method, which will generate a single sql statement to do the job,

We found it harder to answer this question, “Given a list of image ids, what are all the associated venues?”, since :belongs_to does not provide the :through option. We couldn’t see how to get Rails to generate a single sql statement to answer this, although we could write our own by hand.

After some experimentation, we can get it down to two Rails-generated sql statements to answer the question. (But surely its doable in one?)

# given a list of image_ids, get a list of gig_ids, then a list of venue_ids.

gig_ids = Image.find(image_ids, :include => :gig ).map {|i| i.gig_id}.uniq
venue_ids = Gig.find(gig_ids, :include => :venue).map {|g| g.venue.id}.uniq

The use of :include means the :belongs_to can pre-emptively pull in the next model in the relationship as part of the sql generated by find. Incidentally, we did not realise until now that you could pass a list of ids to find and it will incorporate it into the one sql statement.

For the record, here are the models, where each model has a :string column called :name, and the relevant *_id foreign key:

class Image < ActiveRecord::Base
belongs_to :gig

def self.venues( image_ids )
gig_ids = Image.find( image_ids, :include => :gig ).map {|i| i.gig_id}.uniq
Gig.find(gig_ids, :include => :venue).map {|g| g.venue.id}.uniq
end

end

class Gig < ActiveRecord::Base
belongs_to :venue
has_many :images, :dependent => :destroy
end

class Venue < ActiveRecord::Base
has_many :gigs, :dependent => :destroy
has_many :images, :through => :gigs
end

And we quite liked this bit of code in a migration to create some initial dummy data to experiment with. Each venue has 3 gigs, and each gig has 3 images. By using << to hook up all the model instances to each other, e.g. adding a gig to a venue and adding an image to a gig, a single venue.save causes that venue and all its gig instances and all their image instances to be saved as well.

class CreateInitialHierarchy < ActiveRecord::Migration

def self.up

num = 3

(1..num).each do |v|
venue = Venue.new( :name => “venue #{v}”)

(1..num).each do |g|

gig = Gig.new( :name => “gig #{g} for venue #{v}”)

venue.gigs << gig

(1..num).each do |i|

image = Image.new( :name => “image #{i} for gig #{g} for venue #{v}”)

gig.images << image

end

end

venue.save

end

end

def self.down

(1..num).each do |v|

venue = Venue.find_by_name( “venue #{v}” )

if !venue.nil?

venue.destroy

end

end

end

end

1 Comment

Leave a Reply