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
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.