2007-03-01 @ 11:02
Audit Logs with ActiveRecord
I’ve been trying to create an audit log for a few (12) tables, and unfortunately ActiveRecord seems to be falling flat for what I want to do. First I’ll describe whats out there already to do this, then I’ll talk about what I had to do.
There are a couple nice plugins out there for helping you keep track of changes to your records. The first one is acts_as_versioned. acts_as_versioned will copy your record to a version table whenever there is an insert or update, then increase a version number in the original table. It also automatically adds a list of versions to your original model, and lets you revert to any particular version. This is great, and almost exactly what I needed. I didn’t really need the ability to revert to a version, but that just seemed like gravy on top. The only problem with acts_as_versioned comes in when you try to keep track of changes to habtm relationships. But this is where the second plugin comes in to play.
The second plugin is called acts_as_versioned_association. This plugin will help you keep track of changes to your relationships. acts_as_versioned_association is built on top of acts_as_versioned. They way it works is by setting the owner model to acts_as_versioned, then when any associations are updated, it writes a new version of the owner model, then writes the associations to a associations version table. So if you were to have an Article model that has and belongs to many Documents, you would need 5 tables to represent that:
If the association changes, a record is written to articles (to increase the version number), articles_versions, articles_documents, and articles_documents_versions. But what happens if Article has 10 versioned habtm relationships, and just one of those relationships changes? Then a record will get written to every habtm version table for just one change. Thats 12 writes for a change to just one relationship. That will not scale….
Fortunately I don’t care about reverting to a previous version. All I care about is what changed, and when. So my favorite solution for this problem is to add triggers to the tables that may change. That way I only get one extra write when a relationship changes. Just copy the row to another table with a timestamp and an action.
But what about has_many :through?
has_many :through allows you to put a model on top of the join table. Then I could just drop acts_as_versioned on top of that model and be done. I would have used this solution except that I ran in to a bug. has_many :through does not support all of the same array manipulation that habtm does. For example, you can append («) to has_many :through and habtm, but the clear method does not work the same way on has_many :through. Also, has_many :through does not set an attribute= method like habtm does.