When is a record really updated_at in Ruby on Rails (and the under-overlooked Observer class)?
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.
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.
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.
Photo by joeltelling
nice writeup, useful !
sazwqa
December 16, 2008 at 4:48 am
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
David
Good point. Thanks for the comment
ctagg
March 26, 2009 at 10:53 pm