find_or_initialize_by on has_many association causes duplicate error

kenn picture kenn · Jan 10, 2012 · Viewed 9.1k times · Source

I'm seeing a strange error since I moved from Rails 3.0.11 to 3.1.3. Here's a standalone code to reproduce the error:

require 'active_record'

ActiveRecord::Base.establish_connection(
  :adapter  => 'mysql2',
  :username => 'root',
  :database => "some_development"
)

class User < ActiveRecord::Base
  has_many :favorites
end

class Favorite < ActiveRecord::Base
  belongs_to :user
end

u = User.create

# f = u.favorites.find_or_create_by_site_id(123)      #=> pass
f = u.favorites.find_or_initialize_by_site_id(123)    #=> fail
f.some_attr = 'foo'
f.save!

u.name = 'bar'
u.save!                # ActiveRecord::RecordNotUnique will be thrown here!

will end up ActiveRecord::RecordNotUnique attempting to INSERT the same record to the favorites table. (Note that with this example, (user_id, site_id) pair must be unique on favorites)

Interestingly, if I use find_or_create instead of find_or_initialize no exceptions are raised.

In the stack trace I noticed autosave_association gets called, don't know why, but actually has_many :favorites, :autosave => false instead of has_many :favorites removes the error, too. As I've never cared about autosave, I'm not even sure if :autosave => false is a good idea or not.

What am I doing wrong, or is it a Rails bug? Can anyone give me a pointer to look at?

Answer

Thong Kuah picture Thong Kuah · Jan 10, 2012

have you tried not calling f.save! ? u.save! should save both favourites and users.

> f = u.favorites.find_or_initialize_by_site_id(123)

> u.favorites.include?(f)
==> false

> f2 = u.favorites.build(:site_id => 123)

> u.favorites.include?(f2)
==> true

I think what you find is that the new favourite f you have created is a separate object. Hence you will be saving f, while there is another un-saved favourite too in u.favourites. Hence a non-unique error occurs when you save u (which also saves the favourites)

I'm not sure if this is a bug newly introduced in Rails 3.1. It may be intentional.

In Rails 3.0 find_or_initialize_by did not populate the array

> f = u.favorites.find_or_initialize_by_site_id(123)

> u.favorites
==> []

Looks like a bug - see https://github.com/rails/rails/pull/3610