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 :gigdef 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
endend
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
this feed
1 Comment
July 10, 2008 at 8:12 am
Thanks a lot.
Very cool instruction!
Leave a Reply