zargony.com

#![desc = "Random thoughts of a software engineer"]

Vanishing records on creating multiple models in one action

Did you ever want to create multiple models in one action? E.g. if your user model has many email addresses, create a user record and his first email address when he signs up... Nick of Pivotal Blabs wrote a nice article about it -- but it almost drove me to despair this morning when I was trying to break it down to my user and email addresses model.

Basically, it's simple: A user has many (but at least one) email addresses. An email address belongs to a user:

class User < ActiveRecord::Base
  has_many :email_addresses
end
class EmailAddress < ActiveRecord::Base
  belongs_to :user
end

Now, when a new user is created, it should also have his initial email address stored. Normally, you'd do something like the following in your controller's signup action:

# params = { :username => 'test', :email => 'test@example.com' }
user = User.create!(params)
user.email_addresses.create!(params[:email])

Moving the creation of the first email address record into model space looks easy. Just add an email= method to the user model, that creates the first email address record:

class User
  def email= (address)
    email_addresses.build(:address => address)
  end
end

Easy as pie... In the controller, you can now use

User.create!(params)

to create a user record and an associated email address record. As long as you use create! to create the record, everything works fine -- but it all messes up, if you just build the object and save it later. Trying at the rails console shows that the associated email address record does not get saved if you use the collection getter method before saving the record.

Using User.create! works fine:

User.create!(:username => 'test1', :email => 'test1@example.com')
 # INSERT INTO users ('username') VALUES ('test1')
 # INSERT INTO email_addresses ('user_id', 'address') VALUES (1, 'test1@example.com')
 # => #<User id: 1, username: "test1">

Building an object and instantly saving it, does also work fine:

u = User.new(:username => 'test2', :email => 'test2@example.com')
u.save!
 # INSERT INTO users ('username') VALUES ('test2')
 # INSERT INTO email_addresses ('user_id', 'address') VALUES (2, 'test2@example.com')
 # => #<User id: 2, username: "test2">

But: Accessing the list of associated email addresses before saving the record however makes the new email address record vanish:

u = User.new(:username => 'test3', :email => 'test3@example.com')
u.email_addresses
 # => []
u.save!
 # INSERT INTO users ('username') VALUES ('test3')
 # => #<User id: 3, username: "test3">

I don't know what's going on there... I guess that this problem is related to the fact that ActiveRecord caches the collection of has_many associated records. It looks like ActiveRecord's collection cache does not recognize associations that are made during initialization and later. That'd at least explain why this problem only occurs if email= is used during initialization and why it works fine on existing records.

The bottom line is, that you can workaround this problem by explicitly telling ActiveRecord not to cache the collection in email=:

class User
  def email= (address)
    email_addresses(true).build(:address => address)
  end
end

It now works fine:

u = User.new(:username => 'test4', :email => 'test4@example.com')
u.email_addresses
 # => [#<EmailAddress id: nil, user_id: nil, address: "test4@example.com"]
u.save!
 # INSERT INTO users ('username') VALUES ('test4')
 # INSERT INTO email_addresses ('user_id', 'address') VALUES (4, 'test4@example.com')
 # => #<User id: 4, username: "test4">

The question is: Is this is a flaw, a bug or intended behaviour in ActiveRecord? Eventually, I'll file a ticket at the Rails Trac and see, what the ActiveRecord gurus think about it.

Update: I submitted this problem to the Rails Trac: Ticket #9577

Update 2008-04-16: As far as I know, this is still an issue and now reported as Ticket #9 at the new Rails bug tracker.

Update 2008-05-08: The patch to fix this problem was committed to the rails repository today, so this shouldn't be a problem in Rails 2.1 anymore.