Flexible ActiveRecord Versioning with ActsAsRevisable
April 2nd, 2009
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”
Sorry, comments are closed for this article.
April 2nd, 2009 at 10:38 PM
It’s about time. :P
April 3rd, 2009 at 04:24 AM
Great timing! We’re just starting a project where we need decent versioning support and along comes ActsAsRevisable…and it looks excellent :D
April 3rd, 2009 at 09:39 AM
Spelt mah name rong. tx. also, my new domain is voxdolo.me.
Nice work on the release bro.
April 3rd, 2009 at 10:10 AM
Stephen, I disagree, I think you spell it wrong. Fixed anyway.
April 3rd, 2009 at 01:57 PM
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…
April 3rd, 2009 at 02:31 PM
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.
April 8th, 2009 at 01:03 PM
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
April 8th, 2009 at 02:22 PM
Scott, here’s a Gist demonstrating getting at deleted records:
http://gist.github.com/91912
April 9th, 2009 at 04:29 PM
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 :)