Extra level of has_many :through

If I have a model scheme like this:

Company 1---* Customer 1---* Car 1---* Workcard

Or in Rails language:

class Company < ActiveRecord::Base   has_many :customers end

class Customer < ActiveRecord::Base   belongs_to :company   has_many :cars end

class Car < ActiveRecord::Base   belongs_to :customer   has_many :workcards end

class Workcard < ActiveRecord::Base   belongs_to :car end

I can easily extend the associations in Company with

class Company < ActiveRecord::Base   has_many :customers   has_manu :cars, :through => :customers end

But what if I also want a direct associations to Workcard? Like this?

class Company < ActiveRecord::Base   has_many :customers   has_manu :cars, :through => :customers   has_manu :workcards, :through => :cars end

When I do this (assuming, that I've filled the DB with relevant data)

company = Company.find(1) company.customers => Works fine company.cars => Works fine

company.workcards => Gives the following error:

ActiveRecord::StatementInvalid: Mysql::Error: #42S22Unknown column 'cars.company_id' in 'where clause': SELECT workcards.* FROM workcards INNER JOIN cars ON workcards.car_id = cars.id WHERE ((cars.company_id = 1))

So ActiveRecord assumes, that the Car model should have a direct association to Company instead of going through the other "has_many through" association between Company and Cars.

How do I go around and implement this? Should I default to making a finder method

class Company < ActiveRecord::Base   has_many :customers   has_manu :cars, :through => :customers

  def workcards     # Loop through all cars, find related workcards, and return them merged     ...   end end

Or is there a better approach?

- Carsten

Carsten Gehling wrote:

class Company < ActiveRecord::Base   has_many :customers   has_many :cars, :through => :customers   has_many :workcards, :through => :cars end

doesnt work!

yeah. I haven't tried this for a while, but it certainly didn't used to be possible, although I imagine someone could probably code a rails patch to add this functionality.

My solution would be something like;

class Company < ActiveRecord::Base   has_many :customers   has_many :cars, :through => :customers

  def workcards(force_reload=false)     self.cars(force_reload).map{|car| car.workcards(force_reload)}   end end

and make sure that if I need to do this en-masse, instead of;

Company.find(:all, :include => :workcards)

you'd have to go,

Company.find(:all, :include => {:cars => :workcards})

but yeah... I'm gonna play around with this...

must be a reason noone's ever patched rails to allow multi-level :throughs

Matthew Rudy Jacobs wrote:

  def workcards(force_reload=false)     self.cars(force_reload).map{|car| car.workcards(force_reload)}   end

Nice solution. I'll probably never need to do the find(:all) on companies and at the same time load workcards.

One thing though: On associated cars I would be able to do this:

company.cars.find(:all, :conditions => {:model => 'Seat'})

but I cannot do

company.workcards.find(:all, :conditions => {:status => 'open'})

I can do a work-around (I did already and decided I could always refactor when the geniuses at ruby-forum had had their say). I just wanted to find out, if I was severly missing a point here.

must be a reason noone's ever patched rails to allow multi-level :throughs

I've spent more than an hour googling for any sources about this. It almost seems, that noone has ever had the need to do this before. One could argue that, while my db-design is fully 3NF I should proably add company_id as a foreignkey on the workcard model due to performance considerations.

- Carsten

Carsten Gehling wrote:

On associated cars I would be able to do this:

company.cars.find(:all, :conditions => {:model => 'Seat'})

but I cannot do

company.workcards.find(:all, :conditions => {:status => 'open'})

- Carsten

Do you really need a fake proxy for company.workcards?

How about just

def find_workcards(*args)   car_ids = self.cars.map(&:id)   Workcard.send(:with_scope, :find => {:conditions => {:car_id => car_ids}}) do     Workcard.find(*args)   end end

But... if you really want to this may work.

class Company   has_many :cars, :through => :customers

  def workcard(force_reload=false)     return @workcard_proxy if @workcard_proxy && !force_reload     @workcard_proxy = WorkcardProxy.new(self)   end

  class WorkcardProxy

    delegate :each, :, :length, :size, :to => :all

    def initialize(company)       @company = company     end

    def cars       @cars = @company.cars     end

    def all(force_reload=false)       return @all if @all && !force_reload       @all = find(:all)     end

    def scope(&block)       car_ids = self.cars.map(&:id)       Workcard.send(:with_scope, :find => {:conditions => {:car_id => car_ids}}) do         return(yield(&block))       end     end

    def find(*args)       scope do         return Workcard.find(*args)       end     end

    def count(*args)       scope do         return Workcard.count(*args)       end     end   end end

big me up on WWR; http://workingwithrails.com/person/12394-matthew-rudy-jacobs

Matthew Rudy

Matthew Rudy Jacobs wrote:

big me up on WWR; http://workingwithrails.com/person/12394-matthew-rudy-jacobs

Wow.... I will. But I think I have to wait for tomorrow to fully understand your code fully. It's 23.36 here in Denmark, and I am on my third glass of wine. So my brain bailed out on your example. :slight_smile:

But thanks for these very thorough examples. I don't really know yet, if I need a proxy, since your find_workcard method does the trick.

My question actually stemmed from looking at my E/R diagram and thinking relation complexities and performance-issues. That set me on the track of looking for "The Rails Way" on nested associations (beyond the first level done by :through)

On another note: Why is it not possible to do "belongs_to :through..." ?

I know that it's easy to do workcard.car.customer.company.<some-attribute>, but I read about "Preventing train wrecks" in "Advanced Rails Recipes". It gives a solution on the method scope (with delegation), but not an entire association "up through the hierarchy".

- Carsten

Sorry if I am blabbering now - it's probably the wine. :slight_smile:

- Carsten

Carsten Gehling wrote:

On another note: Why is it not possible to do "belongs_to :through..." ?

- Carsten

I dunno, I guess just because it seems like a wild use case that you'd want to build 2nd level associations that way.

say;

house belongs to street belongs to town belongs to state belongs to country

country.houses.create seems reasonable (ish).

but house.create_country seems crazy.

can't see how you'd use it as anything other than a shortcut for multiple delegates.