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.