开发者

has_many :through a has_and_belongs_to_many association

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?


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.


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.


I'm using Rails 3.2.13 and Rasmus, your original setup now seems to work fine on a HABTM.

I'd suggest users try first before attempting a workaround.


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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜