Topic: has_many :through multiple?

I want to have a has_many :through relationship work through another has_many :through relationship on the same model. I can currently achieve this basic effect by using some hackery, but I'd rather avoid it.

class ModelA
  has_many :model_bs
  has_many :model_cs, :through => :model_bs
end

class ModelB
  belongs_to :model_a
  belongs_to :model_c
end

class ModelC
  has_many :model_bs
  has_many :model_as, :through => :model_bs
  belongs_to :model_d
end

class ModelD
  has_many :model_cs
end


I want to be able to call ModelA.find(:first).model_ds. I can achieve this approximately by doing this:

# in ModelA
def model_ds; model_cs.collect(&:model_d).uniq; end

But I'd prefer something cleaner and more Rails-like. Anyone know a better way?

Re: has_many :through multiple?

MB,

I think you've got it just about right. If you want, you can define the method as an association extension, which would make a little bit more sense, but that means changing the interface:

class ModelA
  has_many :model_bs
  has_many :model_cs, :through => :model_bs do
    def model_ds
      proxy_target.collect(&:model_d).uniq
    end
  end
end

class ModelB
  belongs_to :model_a
  belongs_to :model_c
end

class ModelC
  has_many :model_bs
  has_many :model_as, :through => :model_bs
  belongs_to :model_d
end

class ModelD
  has_many :model_cs
end


Which you would use like so:

@model_a.model_cs.model_ds

This way you are calling the "model_ds" method on the model_cs collection, instead of on the model_a object. Some would consider this nit-picking, but I think its easier to understand the resulting client-code, which is the whole point of refactoring.

Re: has_many :through multiple?

Hi guys,

Firstly, thanks for posting this - as a newbie it fixed my initial problem

We have (for a number of good reasons) one case where the chain is five tables long rather than four tables long as above so we have set up the following but could use some help on the 'def' for the proxy_target and how to address is as everything we have tried seems to fail :-(

Thanks in advance!

class ModelA
  has_many :model_bs
  has_many :model_cs, :through => :model_bs
end

class ModelB
  belongs_to :model_a
  belongs_to :model_c
end

class ModelC
  has_many :model_bs
  has_many :model_as, :through => :model_bs
  has_many :model_ds
  has_many :model_es, :through => :model_ds
end

class ModelD
  belongs_to :model_c
  belongs_to :model_e
end

class ModelE
  has_many :model_ds
  has_many :model_cs, :through => :model_ds
end

Re: has_many :through multiple?

Wow I was posting on this topic just last week here trying to help someone else out.

I don't have anything to add here yet but I wanted to post so I can keep my eye on this. (yes, I know about the "Subscribe to this topic" link)

Re: has_many :through multiple?

I guess the question is what are you trying to do? What's the final interface you want? Are to trying to get:

<code>@model_a.model_cs.model_es</code>

Or:

<code>@model_a.model_es</code>

A real world example sure would help too. It would make it easier to understand, and you would therefore get more responses.

Re: has_many :through multiple?

The real world example is for aggregating RSS feeds. There is a relationship that goes

Feed -> Channel Feed <- Channel -> Channel Item <- Item

Where -> represents 1 to Many

Ideally we would find @model_a.model_cs.model_es

The tables Feeds, Channels and Items have attributes but other than than it is exactly as per the Model example above

Re: has_many :through multiple?

class ModelA
  has_many :model_bs
  has_many :model_cs, :through => :model_bs do
    def model_es
      es = []

      proxy_target.each do |c|
        es = es + c.model_es
      end

      es.uniq
    end
  end
end


All the other models can stay the same. That will let you do:

@model_a.model_cs.model_es

That method definition is basically equivalent to the below. I just wrote it like that above to make it self evident what is going on.

def model_es
  (proxy_target.inject([]) { |mem, c| mem += c.model_es }).uniq
end

Let me know if that works.

Re: has_many :through multiple?

I just realized this might work too:

def model_es
  proxy_target.collect(&:model_es).flatten.uniq
end

Re: has_many :through multiple?

All three work a treat, thanks very much

Re: has_many :through multiple?

Spoke to soon.

If a feed has many channels and a channel has many items it correctly reports data from feeds and channels but shows the items from all the channels. i.e. if does not filter the items based on the channel it has. Presumably this needs something like

.... if channel_id = item_channel.channel_id

or something similar added

Re: has_many :through multiple?

I'm not sure I understand. When you call:

@feed.channels.items

you want all the items for all the channels that belong to @feed, right? But instead it is giving you all the items (the same thing you would get if you did Item.all)? Is that what you're saying?

Re: has_many :through multiple?

How does it give you Item.all? Note, I haven't actually tried this to see if that's what you get but if the code works it shouldn't do that... I quess I'm asking why do you say it gives Item.all? Did you set this up?

Re: has_many :through multiple?

datamgt, the curiosity was killing me so I created a rails app to test this out. I set everything up just as I laid out to you (using all three methods), and it worked like a charm. Here are the details of the tests I did:

I created two feeds, one with a name attribute of "Feed A" and the other "Feed B". Then I created four channels with name attributes of "Channel A1", "Channel A2", "Channel B1" and "Channel B2". As you've probably guessed, I associated channel A1 and channel A2 to feed A, and channel B1 and B2 to feed B. Finally, I created four items, each associated to their corresponding channel, so that each channel had an item.

I started up the console and did "feed_a.channels.items", which resulted in the 2 items that were associated with channels A1 and A2. Then I did "feed_b.channels.items", which resulted in the 2 items that were associated with channels B1 and B2. So it seems to have worked just as expected. Let me know if you are getting different results. Cheers.

Re: has_many :through multiple?

Fabio,

Thanks for the responses over night (I have just got up) and I will test them today. Any chance you can tar up your test case and mail it over to davidw at datamgmt dot com so I can check it out. I guess you will be going to bed pretty soon if you are in Vancover :-)

Last edited by datamgmt (2009-06-16 03:26:51)

Re: has_many :through multiple?

Hi,

I just zipped it over to you. Yes, I'm going to bed right now. Good night!

Re: has_many :through multiple?

Fabio,

Great - your example just confirmed I had done everything right.
The error was in the view, which I had modified in debugging to remove the find !
so I had
@feed.channels.item
instead of
@feed.channels.find(channel.id).item
which of course gave the whole array

Re: has_many :through multiple?

So you only wanted the items for a specific channel? That would have worked even without the association extension. I'm confused.

Re: has_many :through multiple?

I need both, I was being unclear in that there are a number of  screens and I needed an entire list and I need a limited list. I started with the limited list and that was fine, then I couldn't get all the items. I then got your input to return everything but no longer got the limited list because of the debugging error. The usual story of chasing ones tail!