1
class Cat < ActiveRecord::Base
    has_many :catbowls
    has_many :bowls, through: :catbowls
end

class CatBowl < ActiveRecord::Base
    belongs_to :cats
    belongs_to :bowls
end

class Bowl < ActiveRecord::Base
    has_many :catbowls
    has_many :cats, through: :catbowls
end

In the rails console I can give a bowl to a cat:

cat = Cat.create(name: 'tibbles')
bowl = Bowl.create(color: 'blue')
cat.catbowls #=> []
cat.catbowls.create(bowl_id: bowl.id)
cat.catbowls #=> [#<Bowl id: 1, color: "blue", created_at: "2014-04-23 22:53:15", updated_at: "2014-04-23 22:53:15">]

This makes sense, and the following association exists:

CatBowl.all #=> [#<Bowl id: 1, cat_id: 1, bowl_id: 1>]

However, here's my problem. If I create the association again, it doesn't actually change the result, but I do get an ineffectual identical catbowl relationship.

cat.catbowls.create(bowl_id: bowl.id)
cat.catbowls #=> [#<Bowl id: 1, color: "blue", created_at: "2014-04-23 22:53:15", updated_at: "2014-04-23 22:53:15">]> 

This relationship is identical to the previous one and utterly useless:

CatBowl.all #=> [#<Bowl id: 1, cat_id: 1, bowl_id: 1>,#<Bowl id: 2, cat_id: 1, bowl_id: 1>]

So how can I stop an existing relationship from being created? What method chain should be used to replace cat.catbowls.create, and create a relationship unless it already exists?

I could use an unless statement to do this,

cat.catbowls.create(bowl_id: bowl.id) unless Catbowl.where(cat_id: cat.id, bowl_id:bowl.id).present?

however this is pretty cumbersome and results in a lot of queries. What I want to do is so common I'm wondering if some rails magic can help me out?

Starkers
  • 10,273
  • 21
  • 95
  • 158

3 Answers3

1

Seems like find_or_create_by is the answer:

Catbowl.find_or_create_by(cat_id: cat.id, bowl_id: bowl.id )

By passing the method the options cat_id and bowl_id, we check for uniqueness concerning only those columns, and ignoring all other columns (such as the id).

Starkers
  • 10,273
  • 21
  • 95
  • 158
  • Just FYI: `Please note *this method is not atomic*, it runs first a SELECT, and if there are no results an INSERT is attempted. If there are other threads or processes there is a race condition between both calls and it could be the case that you end up with two similar records.` http://apidock.com/rails/v4.1.8/ActiveRecord/Relation/find_or_create_by – messanjah Feb 26 '15 at 19:20
  • These days consider `create_or_find_by` instead. See: https://edgeapi.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-create_or_find_by – kkurian Dec 01 '21 at 06:41
1

first remove 's' to cats and bowl do class CatBowl < ActiveRecord::Base belongs_to :cat belongs_to :bowl end

then create bridge entry

cat.bowls << bowl

but it can not prevent repetition. to prevent repetition handle with conditions

condition may likes: (cat.bowls.find_by(id: bowl.id) || cat.bowls << bowl) if rails 4

(cat.bowls.find_by_id(bowl.id) || cat.bowls << bowl) if rails 3

  • Surprising this Rails bug isn't fixed yet. Someone tried, referenced in comments here - http://stackoverflow.com/questions/1315109/rails-idiom-to-avoid-duplicates-in-has-many-through , but no dice. A unique index on the join-table in the DB level will prevent the duplicate, but also throw an error - but better an error than garbage in the DB. This kludge seems the best we have to avoid throwing the error, until this bug is fixed. – JosephK Dec 01 '16 at 12:53
1
cat = Cat.create(name: 'tibbles')
bowl = Bowl.create(color: 'blue')

to simply associate the bowl to the cat:

cat.bowls << bowl 

associate the bowl to cat, but check if relationship exists first:

cat.bowls << bowl unless cat.bowl_ids.include?(bowl.id) 
Chris Lewis
  • 1,315
  • 10
  • 25