I’ve been working on this on and off for the better part of the last year. Life has intruded many times but, I’ve finally got it to a point that I’m comfortable releasing as a 1.0.

This plugin grew out of Stephen Caudill and my use of Rick Olsen’s ActsAsVersioned. The application was centered around versioning of our user’s resources. Early on, AAV worked great for us but, as our requirements got more complicated we found ourselves hacking AAV more and more.

Eventually Stephen came to the conclusion that we needed to break from AAV and roll our own. So he laid out the requirements and I built it. I’m going to talk about all of AAR’s features through a series of posts but, I thought I’d quickly highlight some of it’s unique features now.

Pervasive Callbacks

AAR was created to help build applications that need precise control of the life-cycle of it’s data. In pursuit of that, there is a full suite of callbacks:

  • revisable models
    • before_revise
    • after_revise
    • before_revert
    • after_revert
    • before_changeset
    • after_changeset
    • after_branch_created
    • before_revise_on_destroy
    • after_revise_on_destroy
  • revision models
    • before_restore
    • after_restore
  • both revisable and revision models
    • before_branch
    • after_branch

AAR callbacks act just like ActiveRecord callbacks. They accept all the same arguments and they can prevent the action being taken just like AR callbacks.

Branching and Changesets

Branching is a well understood concept, no need to go in depth on this, here’s the relevant code:

  @branch = @post.branch
  @branch.branch_source == @post # => true

Changesets are a bit more interesting. In non-trivial code you may end up a block of code that may perform many actions that each cause a revision to be saved. If you wish to group those as a single revision, you can:

  @post.changeset do 
    @post.title = "A post"
    # save would normally trigger a revision
    @post.save
    # update_attribute triggers a save triggering a revision (normally)
    @post.updated_attribute(:title, "Another Title")
    # revise! normally forces a revision to be created
    @post.revise!
  end

Deletes can be stored as a revision

This is pretty simple, #destroy no longer destroys the record. It flags it as deleted. Deleted records can then be accessed through the revision class’ #deleted named_scope.

  class Post < ActiveRecord::Base
    acts_as_revisable :on_delete => :revise
  end
  @post.destroy
  PostRevision.deleted # => [@post]

Explicit is better than implicit

ActsAsRevisable will not, by default, define the revision class for you. The most common configuration is as follows:

  class Post < ActiveRecord::Base
    acts_as_revisable
  end

  class PostRevision < ActiveRecord::Base
    acts_as_revision
  end

If you want to call your revision class something else, you can:

  class Post < ActiveRecord::Base
    acts_as_revisable :revision_class_name => 'OldPost'
  end

  class OldPost < ActiveRecord::Base
    acts_as_revision :revisable_class_name => 'Post'
  end

If you absolutely must have AAR generate the class for you:

  class Post < ActiveRecord::Base
    acts_as_revisable :generate_revision_class => true
  end

All data for a model is stored in one table

There isn’t a separate versions table. This comes from the belief that all Posts (or whatever your model happens to be) are Posts whether or not they’re the current version.

This also makes it much easier to find records whether or not they’re the current version or not:

  Post.find(:all, :conditions => {...}, :with_revisions => true)

Wrapping up, requirements and installing

There’s a lot of flexibility here. In the application AAR was built for we were using it to version entire trees of objects together, not just individual records. I plan on digging into this and more in future posts.

AAR 1.0 requires Rails 2.3. You can take a look at the code on GitHub.

  sudo gem install rich-acts_as_revisable --source=http://gems.github.com

Once the gem is installed you’ll want to activate it in your Rails app by adding the following line to config/environment.rb:

  config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"

9 Responses to “Flexible ActiveRecord Versioning with ActsAsRevisable”

  1. Rick Says:

    It’s about time. :P

  2. Rob Anderton Says:

    Great timing! We’re just starting a project where we need decent versioning support and along comes ActsAsRevisable…and it looks excellent :D

  3. Stephen Caudill Says:

    Spelt mah name rong. tx. also, my new domain is voxdolo.me.

    Nice work on the release bro.

  4. Rich Cavanaugh Says:

    Stephen, I disagree, I think you spell it wrong. Fixed anyway.

  5. Brian McManus Says:

    Hey, looking good. You mentioned this but I just wanted to confirm… Would I be able to use this to make a “working copy” of a tree of model objects (as a revision) for a user to hack on before publishing the changes?

    For example if a Recipe had many Steps and Ingredients (or whatever) and the recipe is already published, the user owning the recipe might want to make changes but not have them be publicly viewable till they are finished updating everything…

  6. Rich Cavanaugh Says:

    Brian, yes, you would be able to do that but, AAR doesn’t automate that. It’s just too application specific. AAR simply gives you the control you would need to pull it off.

    For example, Stephen and I accomplished it in our project using the callbacks and changesets. I do plan on writing about this exact case in the future.

  7. Scott Says:

    Question: I’m using this and it’s really, really great. However, if I delete a record–how would I bring that back as an active record at a later time?

    a) I’m still somewhat new to Ruby, and b) I have weird requirements, because my environment sucks.

    Thank you again, Scott

  8. Rich Cavanaugh Says:

    Scott, here’s a Gist demonstrating getting at deleted records:

    http://gist.github.com/91912

  9. Scott Says:

    Thanks Rich. I figured out how to do that–but I was wondering if it’s possible to look at a revision (deleted record, in this case) and tell AAR to make that an active record / restore it.

    In my case, I store account listings from various servers in my database. If accounts disappear, I need to delete them. However, there’s a potential that something happened and an account that still exists simply did not get communicate.

    In such a case, I need to later go back and restore that account (ideally) because I want to maintain the history of revisions for that user.

    Thanks, and sorry for bugging you :)

Sorry, comments are closed for this article.