Pushrod

Old dogs, new tricks

When is a record really updated_at in Ruby on Rails (and the under-overlooked Observer class)?

with 4 comments

Here’s one. When is an item ‘updated’? Well, if you’re using Ruby on Rails, and its automatic timestamps (in particular, updated_at) it’s whenever you save that object. Which is great. One less thing to think about.

However, in the real world, it’s a little more complicated. Take someone’s house. Sure, it changes when the owner changes the core attributes — such as add add another floor to it. But haven’t they also updated it if they give it a makeover, perhaps changed the garden, given it a new front door, painted the windows a different colour, stripped out the mouldings and go all minimalistic inside.

House remodelling —<p> photo by joeltellingPhoto by joeltelling

Depending on how you’ve stored the core attributes of the house (and how much you’ve normalised your models — the house has_many :rooms, has_one :garden, etc), you could completely revamp the house, and it still wouldn’t be updated, at least as far as Rails’ timestamps are concerned. In the case of Autopendium, our old-car website, we’ve had a similar situation. A user’s vehicle has_many :posts (vehicles are essentially blogs devoted to that car, with a few bells and whistles, such as todo lists, associated models and resources, etc).

When would I, as a user, want to be told that a vehicle had been updated? Mainly, when there’s something new been written about it — which, in the case of a restoration, or customisation, usually correlates with something being done to the car. So, we want updated_at to be updated not just when the vehicle record is saved, but also when a new post is created.

Fortunately this is a cinch to solve. If you stop thinking of updated_at as some scary Rails-magic, and think of it as another attribute (albeit one that helpfully gets automatically dealt with when the record is saved), you realise you handle it the same way you’d handle other object whose state depended on that of another one.You could do something like this:

class PostsController < ApplicationController
   def create
     @post = Post.find(params[:id)
     if @post.update_attributes(params[:post])
      @post.vehicle.update_attribute(:updated_at, Time.now)
      ....

A better method is to move it out of the controller and into the Post model. A simple after_create callback should do the trick:

class Post < ActiveRecord::Base
   after_create :timestamp_vehicle
   ....
   private
   def timestamp_vehicle
      vehicle.update_attribute(:updated_at, Time.now) #assumes post belongs_to vehicle and so has a vehicle method
   end

Or you could use the often-overlooked Observer. To quote the Rails API, “Observer classes respond to lifecycle callbacks to implement trigger-like behavior outside the original class.”

So, we create a Observer for the Post model:

class PostObserver < ActiveRecord::Observer
  def after_create(post)
    @post.vehicle.update_attribute(:updated_at, Time.now)
  end
end

In fact, we can slim this down even more, given how the vehicle’s updated_at attribute will be magically updated when we save the vehicle:

class PostObserver < ActiveRecord::Observer
  def after_create(post)
    @post.vehicle.save
  end
end

To activate it we just need to add the following to the environment file in the initialization section:

config.active_record.observers = :post_observer. 

Updated vehicle screenshot

Job done. Both the model callback and the Observer are a helluva lot better than the huge number of SQL craziness when you list a load of vehicles and want to indicate whether each one has been updated (and really is no more an offence against normalisation than counter_cache is).

However, Observers really come into their own when you’ve got multiple models triggering the same behaviour. In the example of a house, you probably want the house to be ‘updated’ when when the colour its painted is changed, when the style is changed, and so on. Here you’d probably wrap it up in a single Observer which watches a whole load of models. Something like:

class UpdatedHouseObserver < ActiveRecord::Observer
  observe Exterior, Style, Garden
  def after_save(record)
    record.house.save
  end
end

I’m also using it in conjunction with my lightweight Facebook library to update a user’s Facebook profile when they add a post or a vehicle (using code in the controller or AR callbacks gets really messy for that).

Update 1: I’ve now posted some details of how I use Observers with the Facebook library.

Update 2: There’s another good post on Observers by Pat Maddox, and specifically a plugin he’s done to make testing with Observers a bit easier.

Advertisements

Written by ctagg

November 9, 2007 at 6:01 pm

4 Responses

Subscribe to comments with RSS.

  1. nice writeup, useful !

    sazwqa

    December 16, 2008 at 4:48 am

  2. Hi,

    Helpful post, thanks.

    One update — now, ActiveRecord records that are not changed do not have their updated_at timestamp updated when saved — record.save does nothing to an unchanged record. You have to do a record.update_attribute(:updated_at, Time.now), as you include above.

    David Reese

    March 26, 2009 at 7:15 pm

  3. Rails 2.3.3, released today, finally has built-in support for a child model to automatically update its parent’s updated_at field when the child is saved! In the association that has the belongs_to association just add:

    # /app/models/comment.rb
    belongs_to :post, :touch => true

    Now when you create/edit/delete a comment then the post it belongs to gets updated_at set to Time.now. You can also name another field if you want:

    # /app/models/comment.rb
    belongs_to :post, :touch => :last_comment_at

    Instead of Post.updated_at being set to now, Post.last_comment_at is set instead.

    Rob Cameron

    July 20, 2009 at 7:40 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: