Creating a history table with Rails and ActiveModel::Dirty

In my rails app I wanted a history table that records all changes to the core entities, recording the object that was changed, the attribute that was changed, the before value and after value.  The aim is that a user can see who has changed this object and when, and over time we can use this in reporting (so, for example, we might care when something became “complete” so we can report completion rate over time.

This post goes through the code to do this – it’s surprisingly simple.

First, we create an entity into which this information will get stored.  We use the standard rails generator, you should make sure that this fits the standards of your app.  You might want to look at my post on customising generator templates so that you can save effort with each new entity.

  rails generate scaffold history object_name:string object_id:integer attribute_name:string before_value:string after_value:string user:references

Then we need to go through each of the entities for which we want history recorded, and adjust the model. You can also create a superclass that holds this so that it’s automatically present on each model, doing that is outside the scope of this particular post.

In the model, add the following:

  include ActiveModel::Dirty
  .....
  before_update :write_history
  .....
  private
  def write_history
    self.changes.each do |attribute_name, values|
      before_value = values[0].to_s.truncate(700) if !values[0].nil?
      after_value = values[1].to_s.truncate(700) if !values[1].nil?
      user_id = self.update_user_id
      History.create!({:object_name => self.class.name,
                       :object_id => self.id,
                       :attribute_name => attribute_name,
                       :before_value => before_value.to_s,
                       :after_value => after_value.to_s,
                       :user_id => user_id},
                      :without_protection => true)
    end
  end

Let’s step through what this is doing.

Firstly, we declare this model to be using ActiveModel::Dirty, which tracks all changes to the model. This will capture, for example, the situation where someone sends an update request to your controller. Your controller typically retrieves the object from the database into the model, then calls update_attributes using the data passed from the UI. ActiveModel::Dirty will record each of those updates, sorting it in a hash of changes. There is reasonably good documentation for ActiveModel:Dirty available here.

Secondly, we declare a callback, requesting that we write_history before any update of this model on the database. We’ve been careful to use before_update, not before_save, as we don’t want to write history for the initial creation. Since we’re storing before_value on each change, any original values are either still on the entity, or they’re the before_value of something that got changed.

Finally, we call the write_history method itself. Here, I’ve chosen to use the changes method on the model, which returns all the changes in a single go, in a hash in the format:

  attr => [original value, new value]
  e.g.
  { "name" => ["bill", "bob"] }

Finally, we write a record into our history table using all the values we got from the changes, plus the class.name of this object, and I’m populating the user_id from a field I have on all my entities – update_user_id. If you don’t have this but you’re using Devise, you could potentially pass current_user into the model in another way.

And that’s it – not so hard!!

Advertisements

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