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) ?
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.
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.
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.
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.
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.
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
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:
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:
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.)
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With