50

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) ?

Cezar
  • 55,636
  • 19
  • 86
  • 87
JCQ
  • 623
  • 1
  • 5
  • 8

3 Answers3

59

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.)

anothermh
  • 9,815
  • 3
  • 33
  • 52
gregates
  • 6,607
  • 1
  • 31
  • 31
  • interesting...and I had tried `source: "Listing"` but that did not work. So the symbol is the only way to set it. – JCQ Aug 09 '13 at 20:26
  • I'm going to mark this as answered (and thanks for the speedy!) though I'm still a little fuzzy on the issue of source: -- I spent a lot of time with the Guide you mention, but the documentation for source is very VERY light. – JCQ Aug 09 '13 at 20:29
  • They key point I think is in the p.s. I added. For an association *through* a join model, you just tell it the name of the association you're joining through (if it can't be inferred). The rest of the options don't matter. Those are for *direct* associations. And that's all `source` does, is specify the association to use on the join model for a `through` association. – gregates Aug 09 '13 at 20:30
  • and I think part of the fuzziness comes from "how is using Source different from using Class_Name" ? Can you clarify a bit more? Why wasn't the user.rb side "source: :user" instead of "class_name: 'User', foreign_key: 'manager_id'" ? – JCQ Aug 09 '13 at 20:32
  • I think the `class_name` and `foreign_key` options on the `Listing` class actually aren't doing any work. They're just being ignored, the real work is being done by ActiveRecord magic. Because the associations on the two models are both called `managers` or (`manager` -- it's smart enough to handle plural/singular differences), it just knows what to do. Try removing those options and it should still work. – gregates Aug 09 '13 at 20:34
  • One caveat is that you *do* need the `class_name` option on the `ListingManager belongs_to :manager` association. That's where it goes, is on the direct association, rather than on the indirect association *through* the `ListManager` model. Then the *through* association just "inherits" that knowledge. – gregates Aug 09 '13 at 20:38
  • **bold** I spoke too soon – JCQ Aug 11 '13 at 15:15
  • I updated the question to reflect a failing test using the solution you suggested (still failing) – JCQ Aug 11 '13 at 17:59
15

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.

Nic
  • 191
  • 1
  • 7
  • 1
    Although this answer is technically the same as the accepted one above, this is clearer and easier to understand to newcomers. – elquimista Nov 16 '18 at 17:33
3

i had several issues with my models, had to add the foreign key as well as the source and class name... this was the only workaround i found:

  has_many :ticket_purchase_details, foreign_key: :ticket_purchase_id, source: :ticket_purchase_details, class_name: 'TicketPurchaseDetail'

  has_many :details, through: :ticket_purchase_details, source: :ticket_purchase, class_name: 'TicketPurchaseDetail'
d1jhoni1b
  • 7,497
  • 1
  • 51
  • 37