views:

225

answers:

3

I am trying to do the following in a Ruby on Rails project:

class FoodItem < ActiveRecord::Base
  has_and_belongs_to_many :food_categories
  has_many :places, :through => :food_categories
end

class FoodCategory < ActiveRecord::Base
  has_and_belongs_to_many :food_items
  belongs_to :place
end

class Place < ActiveRecord::Base  
  has_many :food_categories
  has_many :food_items, :through => :food_category
end

But calling the instance method some_food_item.places gives me the following error:

ActiveRecord::StatementInvalid: PGError: ERROR: column food_categories.food_item_id does not exist LINE 1: ...laces".id = "food_categories".place_id WHERE (("food_cate...

: SELECT "places".* FROM "places" INNER JOIN "food_categories" ON "places".id = "food_categories".place_id WHERE (("food_categories".food_item_id = 1))

Which makes perfect sense - because of the HABTMs on FoodItem and FoodCategory I have the mapping table named food_categories_food_items.

What do I have to do to get some_food_item.places to look places up correctly through the mapping table instead of looking for a food_item_id in the food_categories table?

A: 

A few months ago I wrote an article about this. In short, has_many through a has_and_belongs_to_many association is not allowed by Rails. However, you can partly simulate the relationship by doing something like this:

class FoodItem < ActiveRecord::Base
  has_and_belongs_to_many :food_categories
  named_scope :in_place, lambda{ |place|
    {
      :joins      => :food_categories,
      :conditions => {:food_categories => {:id => place.food_category_ids}},
      :select     => "DISTINCT `food_items`.*" # kill duplicates
    }
  }
end

class FoodCategory < ActiveRecord::Base
  has_and_belongs_to_many :food_items
  belongs_to :place
end

class Place
  has_many :food_categories
  def food_items
    FoodItem.in_place(self)
  end
end

This will give you the some_food_item.places method you seek.

Alex Reisner
A: 

This is correct, because you can't peform "has many through" on a join table. In essence, you're trying to extend the relationship one degree further than you really can. HABTM (has_and_belongs_to_many) is not a very robust solution to most problems.

In your case, I'd recommend adding a model called FoodCategoryItem, and renaming your join table to match. You'll also need to add back the primary key field. Then setup your model associations like this:

class FoodItem < ActiveRecord::Base
  has_many :food_categories, :through => :food_category_items
  has_many :places, :through => :food_categories
end

class FoodCategory < ActiveRecord::Base
  has_many :food_items, :through => :food_category_items
  belongs_to :place
end

class FoodCategoryItems < ActiveRecord::Base
  belongs_to :food_item
  belongs_to :food_category
end

class Place < ActiveRecord::Base  
  has_many :food_categories
  has_many :food_items, :through => :food_categories
end

Note, I also fixed a typo in "Place -> has_many :food_items". This should take care of what you need, and give you the added bonus of being able to add functionality to your FoodCategoryItems "join" model in the future.

Jaime Bellmyer
I can't get this to work - when calling some_food_item.places I get the same error. It's apparently a limitation in ActiveRecord (see http://www.ruby-forum.com/topic/115029).
Rasmus Bang Grouleff
You're right, sorry. I added an answer that fixes everything.
Jaime Bellmyer
+1  A: 

My first version of the answer was incorrect, but this one works perfectly. I made a couple of typos the first time (the hazard of not actually creating an app to test) but this time I verified. And a plugin is needed, but this is easy. first, install the plugin:

script/plugin install git://github.com/ianwhite/nested_has_many_through.git

This installs Ian White's workaround, and it works seamlessly. Now the models, copied directly from the test app I setup to get this working:

class FoodItem < ActiveRecord::Base
  has_many :food_category_items
  has_many :food_categories, :through => :food_category_items
  has_many :places, :through => :food_categories
end

class FoodCategory < ActiveRecord::Base
  has_many :food_category_items
  has_many :food_items, :through => :food_category_items
  belongs_to :place
end

class FoodCategoryItem < ActiveRecord::Base
  belongs_to :food_item
  belongs_to :food_category
end

class Place < ActiveRecord::Base
  has_many :food_categories
  has_many :food_category_items, :through => :food_categories
  has_many :food_items, :through => :food_category_items
end

Now "far" associations work just as well. place_instance.food_items and food_item.places both work flawlessly, as well as the simpler associations involved. Just for reference, here's my schema to show where all the foreign keys go:

create_table "food_categories", :force => true do |t|
  t.string   "name"
  t.integer  "place_id"
  t.datetime "created_at"
  t.datetime "updated_at"
end

create_table "food_category_items", :force => true do |t|
  t.string   "name"
  t.integer  "food_item_id"
  t.integer  "food_category_id"
  t.datetime "created_at"
  t.datetime "updated_at"
end

create_table "food_items", :force => true do |t|
  t.string   "name"
  t.datetime "created_at"
  t.datetime "updated_at"
end

create_table "places", :force => true do |t|
  t.string   "name"
  t.datetime "created_at"
  t.datetime "updated_at"
end

Hope this helps!

UPDATE: This question has come up a few times recently. I wrote an article, nesting your has_many :through relationships, to explain in detail. It even has an accompanying example application on GitHub to download and play around with.

Jaime Bellmyer
It certainly helps!Thank you both for the solution and also for the heads up on HABTM. I'm sure this won't be the only place I'll apply this solution!
Rasmus Bang Grouleff