mrcljx / historical

DRY and serialization-free versioning for ActiveRecord
http://github.com/sirlantis/historical
MIT License
13 stars 0 forks source link

Historical: Versioning and Auditing

There are several plugins available for versioning (e.g. acts_as_versioned, simply_versioned, vestal_versions). Since they try to solve versioning using a relational database they require that you setup table for each model being versioned, clutter your main table or serialize your data into a single TEXT or BLOB field.

Historical doesn't need to look for workarounds since it uses MongoDB as the backend, a document-database which does not require a fixed schema or table structure.

Rails Version

Developed with/for Rails 3.0, installable using Bundler.

Usage Example

# models/message.rb

class Message < ActiveRecord::Base
  # string    :title
  # text      :body
  # datetime  :published_at
  # integer   :author_id

  # This is unnecessary if you use Rails (will be installed by default on boot)
  extend Historical::ActiveRecord

  is_historical
end

# app.rb

m = Message.create(:title => "foo", :author_id => 1)
m.author = Person.find(2)
m.title = "bar"
m.save!

versions = m.history.versions.all

# get old values
versions[0].title       #=> "foo"
versions[0].author_id   #=> 1

# access an old relation
old = versions[0].restore     #=> <#Message>
old.author                    #=> User(id:1)

# what changed?
versions[1].diff.to_hash      #=> { :author_id => [1, 2], :title => ["foo", "bar"] }
versions[1].diff.changes      #=> [<#AttributeDiff>, <#AttributeDiff>]
versions[1].meta.created_at   #=> 2010-01-23 18:56:52 (date when model was saved)

Audits (and other Meta-Data)

As you have seen above each version contains a meta-object. You can write custom data to that meta object.

# YourApp.current_user could be set by a before_filter

class AuditedMessage < ActiveRecord::Base

  # This is unnecessary if you use Rails (will be installed by default on boot)
  extend Historical::ActiveRecord

  is_historical do

    meta do
      # extend that object with MongoMapper helpers
      key :reason, String

      belongs_to_active_record :author, :required => true, :class_name => "Person"
    end

    callback do |version|
      version.meta.author   = YourApp.current_user
      version.meta.reason   = "some reason"
    end
  end
end

Historical::Models::ModelVersion.where(:"meta.author_id" => 1).all

belongs_to_active_record

The MongoMapper extension belongs_to_active_record creates belongs_to (also polymorphic) relations and will handle key generation.

History Model (Quick Overview)

When calling model.history you will get a object that contains several methods to operate with the history of a model.

UseCase for history.next_version

old_message = message.history.original_version.restore
not_so_old_message = old_message.history.next_version.restore

Note: Plucky Query (PQ)

MongoMapper - which is used by Historical - uses Plucky, a query generator. To perform the query you must call .all on it (similar to ActiveRecord).

Interaction with 3rd Party Plugins

Historical won't prevent your model from being destroyed. Should your model be destroyed all versions will be destroyed as well. However you might consider to use is_paranoid by semanticart for that. Historical will then detect updates on the deleted_at column and store a new version.

Note: is_paranoid was discontinued by semanticart in October 2009. I recommend to read about the whys. These are also the reasons why such feature isn't implemented in Historical by itself. You might want to use the less hacky is_paranoid by mislav, who developed will_paginate.

Intellectual Property

Copyright (c) 2010 Marcel Jackwerth (marcel@northdocks.com). Released under the MIT licence.