Dealing with read-only fields and partial updates in Rails 1.x

2 minute read

So, you have a column in your database you can’t update after the record is created. Not don’t want to update, but can’t. Specifically, you might have a column that is protected by a trigger, which will cause an error if that column is included in a update. How do you prevent ActiveRecord from trying to update that column?

Prior to Rails 2.0, ActiveRecord will always generate an SQL UPDATE statement that includes all of the attributes in the model, even if they hadn’t changed.

product = Product.find(:first)
=> #<Product:0x23a7d00 @attributes={"name"=>"Product", "description"=>"Lorem ipsum",  "price"=>"9.99", "sku" => "000001"}>
# Inflation
product.price += 1.00.to_d # You're not using floats for prices, are you?
product.save

=> UPDATE products SET `price` = '10.99', `available` = 1, `description` = 'Lorem ipsum',  `name` = 'Product', `sku` = '000001' WHERE `id` = 1

If “sku” happens to be read-only, the update will fail, and so will your app.

The right way to fix this is to upgrade to Rails 2.x. Starting 2.0 you can use attr_readonly which (silently) removes the attribute from the UPDATE statement.

attr_readonly :sku
product = Product.find(:first)
=> #<Product:0x23a7d00 @attributes={"name"=>"Product", "description"=>"Lorem ipsum",  "price"=>"9.99", "sku" => "000001"}>
product.price += 1.00.to_d
product.save

=> UPDATE products SET `price` = '10.99', `available` = 1, `description` = 'Lorem ipsum',  `name` = 'Product' WHERE `id` = 1

And, starting with 2.1, ActiveRecord only updates attributes that have been changed. As long as you don’t change the value of an attribute, it won’t be included in the UPDATE statement.

product = Product.find(:first)
=> #<Product:0x23a7d00 @attributes={"name"=>"Product", "description"=>"Lorem ipsum",  "price"=>"9.99", "sku" => "000001"}>
product.price  += 1.00.to_d
product.save

=> UPDATE `products` SET `price` = '10.99'  WHERE `id` = 1

(Obviously, it’s better to explicitly mark an attribute as read-only then to depend on this behavior.)

But, what if you are working with a pre 2.X version of Rail? As I said above, ActiveRecord generates the UPDATE statement based on the attributes in the model. The trick, or should I say ugly hack, is to load the record with only the fields you want to update using :select. This way, when the UPDATE is generate it will only include those attributes that were loaded into the record.

product = Product.find(:first, :select => 'id, name, price')
=> #<Product:0x23a7d00 @attributes={"name"=>"Product", "price"=>"9.99", "id"=>"1"}>
product.price  += 1.00.to_d
product.save

=> UPDATE `products` SET `price` = '10.99',   `name` = 'Product' WHERE `id` = 1

When doing this, you need to include the “id” column (or whatever your primary key is) in the select. Also note that while this will work with find_by_ methods, find_or_initialize_by_ methods do not take the :select option.

Yup, it’s ugly but, it does, in fact, work.

Comments