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 G. 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 R. 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 G. 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 R.

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

  • Carsten

Matthew R. 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., 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

Carsten G. 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.