Reputation: 2404
I have three tables Issues, Labels, & IssuesLabel
issues.rb
class Issue < ActiveRecord::Base
has_and_belongs_to_many :labels
end
labels.rb
class Label < ActiveRecord::Base
has_and_belongs_to_many :issues
end
issues_label.rb
class IssuesLabel < ActiveRecord::Base
belongs_to :issue
belongs_to :label
end
When I call issue.labels.find_or_create_by(name: 'bug')
for one issue,
and issue.labels.find_or_create_by(name: 'bug')
for a different issue, it creates two different records in the Labels
table for bug
I'm expecting it to find the existing bug
record and add an entry to the IssuesLabel
join table. What am I missing here?
Upvotes: 0
Views: 388
Reputation: 1604
First note, that has_and_belongs_to_many create a many to many relationship with the party that it connects. Therefore you can have two objects related to each other in this fashion:
issues.rb
class Issue < ActiveRecord::Base
has_and_belongs_to_many :labels
end
labels.rb
class Label < ActiveRecord::Base
has_and_belongs_to_many :issues
end
See section: 2.6 The has_and_belongs_to_many Association of this: http://guides.rubyonrails.org/association_basics.html
In the migration you create the join table, but you do not need to model it.
Now the way you are doing your create will always create a new label.
There is a difference in these two statements:
issue.labels.find_or_create_by(name: 'bug')
and
issue.labels << Label.find_or_create_by(name: 'bug')
The former looks inside the related labels to the issue and creates one if it doesn't exist. For instance, it only queries what issue.labels would return. Therefore when it can not find a related label to the issue with that name, it creates one. Now the latter query does this differently. It looks within the Labels model and says "is there any labels in here with the name 'bug'?" if there are, it finds that label and associates that a label of the issue. If not, it creates a new label before associating it with the issue.
In your case, the latter method will return the result you are looking for.
---- Sub question from Comments about deleting -----
has_and_belongs_to relationships are a 3 table join. When you call:
issue.labels.first
You are listing the first object within the labels collection. This object is a Label, not a join. Removing this, removes the actual object Label that is references in your join table.
issue.labels.first.delete
issue.labels.first.destroy
Both of these are equivalent to calling, assuming there was only one label in your collection (both remove the object, destroy triggers the callbacks):
Label.first.delete
Label.first.destroy
This is not what you are trying to achieve. What you need to do is call is this:
label = Label.first
issue.labels.delete(label)
This leaves the label as is, and just removes the association from the join table.
Upvotes: 2