Active Record


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.

Rick Olson’s attachment_fu is a great plugin for attaching files documents to Rails models. It’s a rewrite of his acts_as_attachment plugin. While it can handle any kind of file data, most commonly, it is used for attaching images; as a result attachment_fu handles automatic resizing of images, and creation of thumbnails using RMagick, minimagick, or ImageScience.

For example:

class ProductImage  < ActiveRecord::Base
  belongs_to :product
  has_attachment :content_type => :image,
                 :storage => :file_system,
                 :path_prefix => '/public/images/products/',
                 :resize_to => '300',
                 :thumbnails => {:thumb => '75x75' }

  validates_as_attachment
end

The above will take an image, resize it to 300 pixels wide (automatically adjusting the height to preserve the original images aspect ratio), and to 75 by 75 pixels for a thumbnail, and save resulting images. Combined with a Product model that has_one :image, or has_many :images, and the right form, you can easily manage your product images.

However, an image with both a fixed width, and fixed height, like our thumbnail, can be a problem. If the original, and resized image do not have the same aspect ratio the resized image will be distorted. In this case, if the original is not square, our thumbnail will be look squished in which ever dimension was longer originally. This is not a problem for the main image because we let the height be calculated automatically.

Fortunately, there is a simple trick that allows us to override the method attachment_fu uses to resize image and manipulate it ourselves. Add the following to the ProductImage model:

  protected

  # Override image resizing method
  def resize_image(img, size)
    # resize_image take size in a number of formats, we just want
    # Strings in the form of "crop: WxH"
    if (size.is_a?(String) && size =~ /^crop: (\\d*)x(\\d*)/i) ||
        (size.is_a?(Array) && size.first.is_a?(String) &&
          size.first =~ /^crop: (\\d*)x(\\d*)/i)
      img.crop_resized!($1.to_i, $2.to_i)
      # We need to save the resized image in the same way the
      # orignal does.
      self.temp_path = write_to_temp_file(img.to_blob)
    else
      super # Otherwise let attachment_fu handle it
    end
  end

and change the thumbnail size to:


:thumbnails => {:thumb => 'crop: 75x75' }

Now, if the image size starts with ‘crop: ‘, the image will be resized and then cropped to fit. Otherwise, it’s passed on to attachment_fu and handed normally. I’m using the RMagic crop_resized! method, which resize the image using the smaller dimension and then crops the large one to fit. If you are using minimagick, or ImageScience you may need to fiddle a bit with the code. Obviously, you can extend this approach to manipulate the image anyway you see fit. For example you could automatically put a border on the images:

  def resize_image(img, size)
    # Add a 2x2 red border and pass the image to attachment_fu
    img.border!(2,2,'red')
    super
  end

Or blur them:

  def resize_image(img, size)
    img = img.blur_image
    super # Pass the blured image to attachment_fu
  end

Or any other weirdness your heart desires. Have fun!