July 1

Watch out for using ActiveRecord’s update_attributes on dirty objects

Posted by mtoledo
Filed under rails, ruby | 2 Comments

I’ve recently found out a very odd particularity about how ActiveRecord behaves when relationship properties through the update_attributes method in ActiveRecord::Base. In fact due to its simple implementation, its actually a behavior of any saving of relationships on dirty records.


# in rails ActiveRecord::Base (base.rb)

# Updates all the attributes from the passed-in Hash and saves the record. If the object is invalid, the saving will
# fail and false will be returned.
def update_attributes(attributes)
  self.attributes = attributes
  save
end

As we can see, update_attributes just updates the attributes property on the model and calls save. The unintuitive behaviour will happen when dealing with an association’s foreign key attribute and its auto generated association method.

To exemplify, I’ll show you this behavior by using the ‘user_id’ foreign key and the ‘user’ association on the ‘task’ model, in a ‘belongs_to’ association:


>> t = Task.new
=> #<Task id: nil, name: nil, category: nil, created_at: nil, updated_at: nil, user_id: nil>
>> t.user = User.find(1)
=> #<User id: 1, login: "mtoledo", ... >
# the association sets the user_id to 1
>> t.user_id
=> 1
# update_attributes the user_id to 3
>> t.update_attributes :user_id => 3, :name => 'Test'
=> true
# we might expect user_id to be 3 now!!
>> t.user_id
=> 1
>> t.save
=> true
>> t.user.id
=> 1

As can be seen from the example above, even though I called ‘update_attribute’ with a user_id of 3, a call to user_id yields 1, the previous value, which you would expect to be overwritten. Saving the object saves it to user with id 1, not 3.

Notice how this behavior is not exclusive of update_attributes and can be reproduced by direct calls to save, where user and user_id conflict. Though, you’ll only realize this after your object has been saved.


>> t = Task.new :name => 'Test'
=> #<Task id: nil, name: "Test", category: nil, created_at: nil, updated_at: nil, user_id: nil>
# set user to that of id 1
>> t.user = User.find(1)
=> #<User id: 1, login: "mtoledo", ..
# set user_id to 3
>> t.user_id = 3
=> 3
# queries to the user_id yield 3 before saving the object
>> t.user_id
=> 3
>> t.save
=> true
# after saving it, though, they yield 1
>> t.user_id
=> 1

Trying to figure out where did rails set the user_id from 3 back to 1, I found out first that it was something orthogonal to the actual save method. When the record is new, save eventually calls the private method ‘create’, which simply adds quotes around the properties of your model and calls insert into to your table, returning the new found id for the row.


# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
  if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
   self.id = connection.next_sequence_value(self.class.sequence_name)
 end

  quoted_attributes = attributes_with_quotes

  statement = if quoted_attributes.empty?
    connection.empty_insert_statement(self.class.table_name)
  else
    "INSERT INTO #{self.class.quoted_table_name} " +
    "(#{quoted_column_names.join(', ')}) " +
    "VALUES(#{quoted_attributes.values.join(', ')})"
  end

  self.id = connection.insert(statement, "#{self.class.name} Create",
  self.class.primary_key, self.id, self.class.sequence_name)

  @new_record = false
  id
end

No sign of any replacement of values. Trying to call the ‘attributes_with_quotes’ method he uses for his query on my recently created object shows the following:


>> t.name = 'Test'
=> "Test"
>> t.user = User.find(1)
=> #<User id: 1, login: "mtoledo", ...
>> t.user_id = 3
=> 3
>> t.send(:attributes_with_quotes)
=> {"name"=>"'Test'", "category"=>"NULL", "updated_at"=>"NULL", "user_id"=>"3", "created_at"=>"NULL"}

Notice user_id is 3. Since he uses this value on the INSERT INTO statement, its odd that it contrasts with my rails log’s id of 1:


-- Task Create (3.2ms)
INSERT INTO `tasks` (`name`, `category`, `updated_at`, `user_id`, `created_at`)
VALUES('Test', NULL, '2009-07-01 21:52:52', 1, '2009-07-01 21:52:52')

Notice though that ‘user_id’ is not the only difference. The ‘created_at’ and ‘updated_at’ attributes are also set on the logs, but nowhere to be seen on create. This means some hooks might be at play here.

Digging into ActiveRecord’s declaration of ‘belongs_to’ method (which is the one used to declare the user association above) in associations.rb, we find out what happens to the association. If the object is already saved, it assigns its id’s value to the foreign key:

# rails: associations.rb
def belongs_to(association_id, options = {})
  # ... omitted: some STI stuff
  else
   association_accessor_methods(reflection, BelongsToAssociation)
   association_constructor_method(:build,  reflection, BelongsToAssociation)
   association_constructor_method(:create, reflection, BelongsToAssociation)

    method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
  define_method(method_name) do
    association = instance_variable_get(ivar) if instance_variable_defined?(ivar)

    if !association.nil?
      if association.new_record?
        association.save(true)
      end

      if association.updated?
        self[reflection.primary_key_name] = association.id # <== there you go
      end
    end
  end
  before_save method_name
end

Adding this dynamically declared method as a before_save hook, rails guarantees that whatever was set for the belongs_to association will override the foreign key on save.

Given that we may favor manipulating the foreign key (user_id) directly than the association (user) in order to prevent additional database queries, but also that we might not have been the only one involved in the object’s life cycle, and also given rails’ attitude of not throwing errors when things might go wrong, its important to keep this behavior in mind.

This entry was posted on Wednesday, July 1st, 2009 at 10:48 pm and is filed under rails, ruby. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

2 Responses to “Watch out for using ActiveRecord’s update_attributes on dirty objects”

  1. rick on July 2nd, 2009 at 4:56 pm

    The solution is simple, don’t update the association and the foreign key to different values.

  2. mtoledo on July 2nd, 2009 at 5:16 pm

    Hi Rick!

    You’re absolutely right. Like I said in the end of the post, though, sometimes you’re not in control of the whole lifecycle of the object, so you might not have control over if the association has been set before you tried to change the foreign key. And you also can’t know if it will work or not until you save your object.

Leave a Reply