Simple association versioning with acts_as_revisable
May 12th, 2009
One of the reasons AAR was created was due to the hoops that we had to jump through to effectively version across associations while taking into account our business logic. We ended up with hundreds of lines of very complicated spaghetti code. It wasn't pretty.
Here I'm going to demonstrate how you can very simply version two models together.
We'll provide for Posts which can have multiple Links. Any time a Post is created or modified, a revision of it and it's associated links at the time will be stored. Adding a link to a Post will also trigger a revision.
Changing a link will not trigger a revision of the Post. This is trivial to add and should be considered a task for you (if you want to play with AAR).
Model walk-through
You can grab the complete code for this any time at GitHub but, I'm going to walk through the models now.
post.rb
class Post < ActiveRecord::Base
acts_as_revisable
has_many :links, :before_add => :revise!
after_revise :revise_links!
private
def revise_links!
self.links.each(&:revise!)
end
end
So first, we're defining our Post, making it revisable and associating it to it's links. We've also added two callbacks.
The first, on the links association, causes the post to be revised before a new link is added. This allows us to grab the state of the links before it changes. So we'll end up with an accurate snapshot of the post.
The second callback is the after_revise callback which simply loops through the links, forcing a revision on them. This allows us to peg a particular post revision with a particular link revision.
Notice there's really no magic here, we're just using the tools acts_as_revisable gives us.
link.rb
class Link < ActiveRecord::Base
acts_as_revisable
belongs_to :post
end
Simply declaring our Link, making it revisable and associating it with a post. Nothing special here.
post_revision.rb
class PostRevision < ActiveRecord::Base
acts_as_revision
has_many :links, :class_name => "LinkRevision", :foreign_key => "post_id"
end
Now we're explicitly defining our PostRevision class and letting AAR know. Defining the association requires a few more options just to override some of ActiveRecord's defaults. Notice PostRevisions will always be associated with LinkRevisions.
link_revision.rb
class LinkRevision < ActiveRecord::Base
acts_as_revision
belongs_to :post, :class_name => "PostRevision"
before_create :reassociate_with_post, :if => :post_in_revision?
private
def reassociate_with_post
self.post = self.current_revision.post.find_revision(:previous)
end
def post_in_revision?
self.current_revision.post.in_revision?
end
end
Here's the most complicated part and even this isn't so bad. We're explicitly defining our LinkRevision and it's association.
Next, we have a before_create callback. Keep in mind, this is the revision model so, before_create in this case is roughly equivalent to before_revise on the Link model. I've defined it this way for the sake of clarity. The work in this callback really concerns the revisions not the revisable. So, while it could be defined in either model, it makes the most sense when reading the code for it to be here.
The reassociate_with_post method being called by the callback is relatively easy to understand if you keep the big picture in mind. A post's links are being revised after the post so, this callback is called after the post has been revised. So, we're simply grabbing the last revision of this post and associating the LinkRevision with that.
Let's see it in action
>> p = Post.create(:title => "Simple association versioning with acts_as_revisable")
=> #<Post id: 55, title: "Simple association versioning with acts_as_revisabl...">
>> p.links.create(:url => "http://github.com/rich/aar-demo-1/tree/master")
=> #<Link id: 92, url: "http://github.com/rich/aar-demo-1/tree/master">
>> p.revisions.size
=> 1
>> p.revisions.first.links
=> []
>> p.links
=> [#<Link id: 92, url: "http://github.com/rich/aar-demo-1/tree/master">]
>> p.title = "That was easy"
=> "That was easy"
>> p.save
=> true
>> p.revisions.map {|r| r.links.size}
=> [1, 0]
>> p.revisions.first.links.first
=> #<LinkRevision id: 93, url: "http://github.com/rich/aar-demo-1/tree/master">
>> p.links.create(:url => "http://github.com/rich")
=> #<Link id: 95, url: "http://github.com/rich">
>> p.revisions.map {|r| r.links.size}
=> [1, 1, 0]
>> p.links.size
=> 2
>> p.links.create(:url => "http://github.com/technoweenie/acts_as_versioned/tree/master")
=> #<Link id: 98, url: "http://github.com/technoweenie/acts_as_versioned/tr...">
>> p.links.size
=> 3
>> p.revisions.map {|r| r.links.size}
=> [2, 1, 1, 0]
I have a couple ideas in progress for a followup post but, I'd love some requests if there's interest.
Remember, you can grab the code on GitHub.
3 Responses to “Simple association versioning with acts_as_revisable”
Sorry, comments are closed for this article.
May 22nd, 2009 at 06:21 PM
I'd like to say that I really appreciate your development of AAR. I went from knowing nothing about Ruby/Rails to having a full datawarehouse / auditing facility in production, and AAR has been invaluable in tracking various changes.
Something I'd really like to see is for AAR to look for 'reference id' fields and to store additional columns, which records the revisablenumber of the foreign record (NULL if none) when you create a revision.
The obvious response is "do it yourself" and I plan to, along with some patches / documentation enhancements to Thinking Sphinx. If you have any suggestions about how to go about implementing such a change--I'd love to hear it.
Thanks again.
June 4th, 2009 at 11:58 PM
Hi, cool post. I have been wondering about this topic,so thanks for writing.
June 5th, 2009 at 11:33 AM
Hi Rich!
First - a great plugin/gem. Really happy with easyness of use.
Actually I'm facing one problem right now. Just-saved object has version 0. But when I update this 0-th version with new data, both records in database have versionable_number=1.
Can you help me or explain this one?
Thanks!