SyndeoLabs

innovation recreation.

That Old Rails Serialize Method

I've been a big fan of the serialization method in ActiveRecord for awhile now, even though I routinely hear some negative feedback about it being on the slow side. In case you don't use serialize much, it's essentially a way to store whole objects in your ActiveRecord-powered database table. When do we use this? Well, because syndeo handles a lot of social-networking-type projects, the first example that comes to mind is the ubiquitous friend_cache.

Here's the scenario:

When CurrentUser is viewing a whole list of fellow members on the average social network, the application will often have a visual indicator of whether User A, B, C or D are your friends. This can become expensive fairly quickly if CurrentUser happens to be looking at a long list, because we'd have to check each user for friendship-status in turn. (Or we could do a single big lookup with all of the user IDs, but for the purposes of this article, let's ignore that option.)

Using serialize solves this problem rather handily because we could store the IDs of all CurrentUser's friends right in his database record. In User.rb, it's a simple as adding:

class User < ActiveRecord::Base

serialize :friend_ids_cache, Array

end

Any data in the friend IDs array is stored in YAML form in the friend_ids_cache field in the database table. When you pull it back out, it's automatically converted back to an array for you to operate on.

We would also need to create some simple convenience methods to add and remove friends from the cache, like so:

def add_to_friend_ids_cache(friend_id)
self.friend_ids_cache << friend_id unless self.friend_ids_cache.include?(friend_id)
self.save
end

def remove_from_friend_ids_cache(friend_id)
self.friend_ids_cache.delete(friend_id) if self.friend_ids_cache.include?(friend_id)
self.save
end

Ideally, you would call these methods from your Friendship model, via the after_create and after_destroy filters, respectively. There is one problem with these methods above though, and it's the fact that the "<<" and #delete methods aren't actually changing the actual attributes of the User instance; they're simply changing the contents of the derived array. (In other words, they don't work.)

This stumped me for a while. There was no way I was going to fully reinitialize the cache every time I added or removed a value in it, as it seemd to defeat the purpose of caching in the first place. Thankfully, I discovered the _will_change! method for handling dirty objects, which turned out to be the solution I was looking for. All we need to do is flag friend_ids_cache as a "dirty" field first, before we can properly update it.

def add_to_friend_ids_cache(friend_id)
self.friend_ids_cache_will_change!
self.friend_ids_cache << friend_id unless self.friend_ids_cache.include?(friend_id)
self.save
end
def remove_from_friend_ids_cache(friend_id)
self.friend_ids_cache_will_change!
self.friend_ids_cache.delete(friend_id) if self.friend_ids_cache.include?(friend_id)
self.save
end

That works now. (Note that the _will_change! method only takes effect until the first save. Afterwards, the field reverts to its clean state and you have to re-flag it for any further dirty manipulation.)

blog comments powered by Disqus

syndeo::media syndeo::media is a social software lab operating out of Manila, Philippines and Sydney, Australia