Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

has_many :through with class_name and foreign_key

I'm working with a fairly straightforward has_many through: situation where I can make the class_name/foreign_key parameters work in one direction but not the other. Perhaps you can help me out. (p.s. I'm using Rails 4 if that makes a diff):

English: A User manages many Listings through ListingManager, and a Listing is managed by many Users through ListingManager. Listing manager has some data fields, not germane to this question, so I edited them out in the below code

Here's the simple part which works:

class User < ActiveRecord::Base   has_many :listing_managers   has_many :listings, through: :listing_managers end  class Listing < ActiveRecord::Base   has_many :listing_managers   has_many :managers, through: :listing_managers, class_name: "User", foreign_key: "manager_id" end  class ListingManager < ActiveRecord::Base   belongs_to :listing   belongs_to :manager, class_name:"User"    attr_accessible :listing_id, :manager_id end 

as you can guess from above the ListingManager table looks like:

create_table "listing_managers", force: true do |t|   t.integer  "listing_id"   t.integer  "manager_id" end 

so the only non-simple here is that ListingManager uses manager_id rather than user_id

Anyway, the above works. I can call user.listings to get the Listings associated with the user, and I can call listing.managers to get the managers associated with the listing.

However (and here's the question), I decided it wasn't terribly meaningful to say user.listings since a user can also "own" rather than "manage" listings, what I really wanted was user.managed_listings so I tweaked user.rb to change has_many :listings, through: :listing_managers to has_many :managed_listings, through: :listing_managers, class_name: "Listing", foreign_key: "listing_id"

This is an exact analogy to the code in listing.rb above, so I thought this should work right off. Instead my rspec test of this barfs by saying ActiveRecord::HasManyThroughSourceAssociationNotFoundError: Could not find the source association(s) :managed_listing or :managed_listings in model ListingManager. Try 'has_many :managed_listings, :through => :listing_managers, :source => <name>'. Is it one of :listing or :manager?

the test being:

it "manages many managed_listings"  do   user = FactoryGirl.build(:user)   l1 = FactoryGirl.build(:listing)   l2 = FactoryGirl.build(:listing)        user.managed_listings << l1   user.managed_listings << l2   expect( @user.managed_listings.size ).to eq 2 end 

Now, I'm convinced I know nothing. Yes, I guess I could do an alias, but I'm bothered that the same technique used in listing.rb doesn't seem to work in user.rb. Can you help explain?

UPDATE: I updated the code to reflect @gregates suggestions, but I'm still running into a problem: I wrote an additional test which fails (and confirmed by "hand"-tesing in the Rails console). When one writes a test like this:

it "manages many managed_listings"  do   l1 = FactoryGirl.create(:listing)   @user = User.last   ListingManager.destroy_all   @before_count = ListingManager.count   expect(  @before_count ).to eq 0   lm = FactoryGirl.create(:listing_manager, manager_id: @user.id, listing_id: l1.id)     expect( @user.managed_listings.count ).to eq 1 end 

The above fails. Rails generates the error PG::UndefinedColumn: ERROR: column listing_managers.user_id does not exist (It should be looking for 'listing_managers.manager_id'). So I think there's still an error on the User side of the association. In user.rb's has_many :managed_listings, through: :listing_managers, source: :listing, how does User know to use manager_id to get to its Listing(s) ?

like image 625
JCQ Avatar asked Aug 09 '13 20:08

JCQ


People also ask

How would you choose between Belongs_to and Has_one?

They essentially do the same thing, the only difference is what side of the relationship you are on. If a User has a Profile , then in the User class you'd have has_one :profile and in the Profile class you'd have belongs_to :user . To determine who "has" the other object, look at where the foreign key is.

What has and belongs to many or has many through?

Stories can belong to many categories. Categories can have many stories. has_many :through gives you a third model which can be used to store various other pieces of information which don't belong to either of the original models. Person can subscribe to many magazines.

What is association in ruby on Rails?

Association in Rails defines the relationship between models. It is also the connection between two Active Record models. To figure out the relationship between models, we have to determine the types of relationship.

What is source in Rails?

The source is used when you need Rails to know that you have creatively used has _many: through association. For example, a post can have many authors (but still one editor). We'll need to create a new table post_authorings.

What is the default foreign_key for a class with many associations?

By default this is guessed to be the name of this class in lower-case and “_id” suffixed. So a Personclass that makes a #has_manyassociation will use “person_id” as the default :foreign_key. If you are going to modify the association (rather than just read from it), then it is a good idea to set the :inverse_ofoption.

What is the default foreign key for has_many Association?

So a Personclass that makes a #has_manyassociation will use “person_id” as the default :foreign_key. If you are going to modify the association (rather than just read from it), then it is a good idea to set the :inverse_ofoption. :foreign_type

How to set the name of a foreign key in rails?

By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix _id added. The :foreign_key option lets you set the name of the foreign key directly:

How do I add a foreign key to a join table?

By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to the other model is the name of that model with the suffix _id added. The :association_foreign_key option lets you set the name of the foreign key directly:


2 Answers

The issue here is that in

has_many :managers, through: :listing_managers 

ActiveRecord can infer that the name of the association on the join model (:listing_managers) because it has the same name as the has_many :through association you're defining. That is, both listings and listing_mangers have many managers.

But that's not the case in your other association. There, a listing_manager has_many :listings, but a user has_many :managed_listings. So ActiveRecord is unable to infer the name of the association on ListingManager that it should use.

This is what the :source option is for (see http://guides.rubyonrails.org/association_basics.html#has-many-association-reference). So the correct declaration would be:

has_many :managed_listings, through: :listing_managers, source: :listing 

(p.s. you don't actually need the :foreign_key or :class_name options on the other has_many :through. You'd use those to define direct associations, and then all you need on a has_many :through is to point to the correct association on the :through model.)

like image 51
gregates Avatar answered Sep 21 '22 08:09

gregates


I know this is an old question, but I just spent some time running into the same errors and finally figured it out. This is what I did:

class User < ActiveRecord::Base   has_many :listing_managers   has_many :managed_listings, through: :listing_managers, source: :listing end  class Listing < ActiveRecord::Base   has_many :listing_managers   has_many :managers, through: :listing_managers, source: :user end  class ListingManager < ActiveRecord::Base   belongs_to :listing   belongs_to :user end 

This is what the ListingManager join table looks like:

create_table :listing_managers do |t|   t.integer :listing_id   t.integer :user_id end 

Hope this helps future searchers.

like image 37
Nic Avatar answered Sep 23 '22 08:09

Nic