www.flickr.com
|
Monday, April 30, 2007
Rails realities part 25 (Do what you say. updating activerecord update_attribute)
I've been working on adding a data field or two on some of my user objects but these fields are really only ever editable by an administrator. You may argue that such fields belong in an entirely different table. Well in the interest of keeping things simple I chose to add it to the existing user object as it is directly applicable.
Anyway, illustrating by example let's say I have a reservation system and after a user creates a reservation an admin user needs to perform some manual operations and then update the reservation as being confirmed. Meanwhile some attributes on this reservation are still editable by the end user.
Ok so let's look at the active record API and we see there's a method called "update_attribute" that takes the name of the attribute and the value you'd like to set for the attribute.
Alright so I just call:
Right?
Well yes that does indeed update the targeted attribute, BUT (and you knew there'd be a but otherwise there's no blog post), it also saves the entire object which updates all attributes.
Here's the active record code for update_attribute
The send call of course sets the attribute value and save eventually boils down to this:
As you can see update simply updates all attributes for the current object.
The quoted_comma_pair_list simply takes the attribute values and builds up the SQL parameter list, e.g. "id = 15, `confirmed` = 1", etc
OK so what's the problem here? Well none if you never care about concurrent editing of the data. "But what about optimistic/pessimistic concurrency?" you ask. Indeed we could use either of those approaches to solve the race condition of two users editing this data simultaneously, but in this instance it's really too heavy handed and doesn't really fit the situation.
As I mentioned at the outset we have a single column named "confirmed", that we'd like to have updated by an administrator. Updating this field would never conflict with a user editing the other fields for this object. Ideally what we'd like is to have is some way to update a single attribute's value... hmm.... perhaps an instance method that when invoked only updates the specified attribute. Hmm... what would we call such a method? Oh, I know, "update_attribute". Uh oh! Who smells an active record mixin patch? In the words of Matt Puppet "I'm about to hack a bitch". I don't think a plugin is the right thing here as I think this is a bug as opposed to a feature.
The above is my new implementation of "update_attribute" that performs as advertised. It will only update the specified column value for the given object. I haven't modified this in the rails codebase and run the tests to see if there's any breakage but I imagine anything that breaks as a result of this behavior change was using the API incorrectly to begin with as it means there would be reliance on the fact that an entire object save is performed when you update a single attribute value. I can't think of scenarios where this would be required behavior, please correct me if I'm not thinking of them.
So now when you add this patch you'll see "UPDATE reservations SET `confirmed` = 1 WHERE id = 20" instead of an update that includes all attributes.
I did a search and sure enough there are some others that agree that the behavior change I've prescribed is the correct thing to do:
http://dev.rubyonrails.org/ticket/8053
There is also a recent discussion on the rails core mailing list on this topic but mostly related to the lack of validation triggering for this call. On that very thread Peter Marklund discusses his need to write a custom method called "update_column" for the very same purpose I've described here.
Anyway, enjoy and happy updating.
Anyway, illustrating by example let's say I have a reservation system and after a user creates a reservation an admin user needs to perform some manual operations and then update the reservation as being confirmed. Meanwhile some attributes on this reservation are still editable by the end user.
Ok so let's look at the active record API and we see there's a method called "update_attribute" that takes the name of the attribute and the value you'd like to set for the attribute.
Alright so I just call:
reservation.update_attribute('confirmed', true)
Right?
Well yes that does indeed update the targeted attribute, BUT (and you knew there'd be a but otherwise there's no blog post), it also saves the entire object which updates all attributes.
Here's the active record code for update_attribute
def update_attribute(name, value)
send(name.to_s + '=', value)
save
end
The send call of course sets the attribute value and save eventually boils down to this:
def update
connection.update(
"UPDATE #{self.class.table_name} " +
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
"WHERE #{self.class.primary_key} = #{quote(id)}",
"#{self.class.name} Update"
)
return true
end
As you can see update simply updates all attributes for the current object.
The quoted_comma_pair_list simply takes the attribute values and builds up the SQL parameter list, e.g. "id = 15, `confirmed` = 1", etc
OK so what's the problem here? Well none if you never care about concurrent editing of the data. "But what about optimistic/pessimistic concurrency?" you ask. Indeed we could use either of those approaches to solve the race condition of two users editing this data simultaneously, but in this instance it's really too heavy handed and doesn't really fit the situation.
As I mentioned at the outset we have a single column named "confirmed", that we'd like to have updated by an administrator. Updating this field would never conflict with a user editing the other fields for this object. Ideally what we'd like is to have is some way to update a single attribute's value... hmm.... perhaps an instance method that when invoked only updates the specified attribute. Hmm... what would we call such a method? Oh, I know, "update_attribute". Uh oh! Who smells an active record mixin patch? In the words of Matt Puppet "I'm about to hack a bitch". I don't think a plugin is the right thing here as I think this is a bug as opposed to a feature.
# put in lib/active_record_ext/base.rb
# append "require 'active_record_ext/base'" to your config/environment.rb
module ActiveRecord
class Base
# Updates a single attribute and saves the record. This is especially useful for boolean flags on existing records.
# Note: This method is overwritten by the Validation module that'll make sure that updates made with this method
# doesn't get subjected to validation checks. Hence, attributes can be updated even if the full object isn't valid.
def update_attribute(name, value)
send(name.to_s + '=', value)
# safely escape the value before we update
atts = attributes_with_quotes(false)
connection.update(
"UPDATE #{self.class.table_name} " +
"SET #{quoted_comma_pair_list(connection, {name => atts[name]})} " +
"WHERE #{self.class.primary_key} = #{quote(id)}",
"#{self.class.name} Update"
)
return true
end
end
end
The above is my new implementation of "update_attribute" that performs as advertised. It will only update the specified column value for the given object. I haven't modified this in the rails codebase and run the tests to see if there's any breakage but I imagine anything that breaks as a result of this behavior change was using the API incorrectly to begin with as it means there would be reliance on the fact that an entire object save is performed when you update a single attribute value. I can't think of scenarios where this would be required behavior, please correct me if I'm not thinking of them.
So now when you add this patch you'll see "UPDATE reservations SET `confirmed` = 1 WHERE id = 20" instead of an update that includes all attributes.
I did a search and sure enough there are some others that agree that the behavior change I've prescribed is the correct thing to do:
http://dev.rubyonrails.org/ticket/8053
There is also a recent discussion on the rails core mailing list on this topic but mostly related to the lack of validation triggering for this call. On that very thread Peter Marklund discusses his need to write a custom method called "update_column" for the very same purpose I've described here.
Anyway, enjoy and happy updating.
Comments:
<< Home
Thanks alot for the snippet! I found it very useful and time-saving.. Rails definitely still needs some core-level optimizations..
<< Home
Post a Comment