hummmingbear
hummmingbear

Reputation: 2404

has_and_belongs_to_many creating duplicate entries in model

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

Answers (1)

ABrowne
ABrowne

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

Related Questions