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) ?
Solution 1:
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.)
Solution 2:
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.
Solution 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'