has_many_versions
This attempts to provide versioning for ‘has many’ relationships within ActiveRecord.
Installation
script/plugin install git://github.com/joshbuddy/has_many_versions.git
Usage
Here is a pretty typical relationship.
class Book < ActiveRecord::Base
belongs_to :author
validates_associated :author
end
class Author < ActiveRecord::Base
has_many :books
end
Lets say we want to include versioning now to modifications made to the collection of books. We can modify the has_many
relationship the following way:
has_many :books, :extend => HasManyVersions
Now changes made to the relationship with the normal <<
and delete
methods will be tracked. There are some modifications needed for both tables for this to work. The owner of the relationship (in this case Author
) needs an integer column named version
(with a default value of one). The collection (in this case Book
) needs two columns, initial_version
and version
.
Assuming we’ve done all that, lets try out some magical versioning to see how it all works.
We’ll start by defining some books.
the_eyre_affair = Book.new(:name => 'The Eyre Affair')
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "The Eyre Affair">
lost_in_a_good_book = Book.new(:name => 'Lost in a Good Book')
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "Lost in a Good Book">
the_well_of_lost_plots = Book.new(:name => 'The Well of Lost Plots')
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "The Well of Lost Plots">
something_rotten = Book.new(:name => 'Something Rotten')
=> #<Book id: nil, initial_version: nil, version: nil, author_id: nil, name: "Something Rotten">
And an author to take them.
jasper = Author.new(:name => 'Jasper Fforde')
=> #<Author id: nil, version: 1, name: "Jasper Fforde">
That sounds good. Just so we’re all on the same page, how does Jasper look at the moment?
jasper.version
=> 1
jasper.books
=> []
Version 1 is where all versioned objects initially start.
Now, lets start adding some books.
jasper.books.push(the_eyre_affair, something_rotten)
And our new version?
jasper.version
=> 2
And our books?
jasper.books
=> [#<Book id: 1, initial_version: 2, version: 2, author_id: 1, name: "The Eyre Affair">,
#<Book id: 2, initial_version: 2, version: 2, author_id: 1, name: "Something Rotten">]
Yeah! Everything is good and green.
Lets add on some more books.
jasper.books.push(lost_in_a_good_book)
jasper.version
=> 3
jasper.books.push(the_well_of_lost_plots)
jasper.version
=> 4
Because we did them one at a time, the version number incremented for each one.
Now, Jasper has a lot of books.
jasper.books
=> [#<Book id: 1, initial_version: 2, version: 4, author_id: 1, name: "The Eyre Affair">,
#<Book id: 2, initial_version: 2, version: 4, author_id: 1, name: "Something Rotten">,
#<Book id: 3, initial_version: 3, version: 4, author_id: 1, name: "Lost in a Good Book">,
#<Book id: 4, initial_version: 4, version: 4, author_id: 1, name: "The Well of Lost Plots">]
Lets take one away…
jasper.books.delete(lost_in_a_good_book)
And the version has incremented
jasper.version
=> 5
jasper.books
=> [#<Book id: 1, initial_version: 2, version: 5, author_id: 1, name: "The Eyre Affair">,
#<Book id: 2, initial_version: 2, version: 5, author_id: 1, name: "Something Rotten">,
#<Book id: 4, initial_version: 4, version: 5, author_id: 1, name: "The Well of Lost Plots">]
But thats not right, he did write that book. Lets go back in time.
jasper.books.rollback
And the version get incremented…
jasper.version
=> 6
And the books…
jasper.books
=> [#<Book id: 5, initial_version: 6, version: 6, author_id: 1, name: "The Eyre Affair">,
#<Book id: 6, initial_version: 6, version: 6, author_id: 1, name: "Something Rotten">,
#<Book id: 7, initial_version: 6, version: 6, author_id: 1, name: "Lost in a Good Book">,
#<Book id: 8, initial_version: 6, version: 6, author_id: 1, name: "The Well of Lost Plots">]
Are right where we left them!
In fact, lets roll it right back to version 2.
jasper.books.rollback(2)
jasper.version
=> 7
jasper.books
=> [#<Book id: 9, initial_version: 7, version: 7, author_id: 1, name: "The Eyre Affair">,
#<Book id: 10, initial_version: 7, version: 7, author_id: 1, name: "Something Rotten">]
And what would the world look like if Jasper Fforde hadn’t written anything?
jasper.books.rollback(1) #back to version one
jasper.version
=> 8
jasper.books
=> []
Of course, it’s always fun to reminisce
jasper.books.at(3)
=> [#<Book id: 27, author_id: 3, initial_version: 2, version: 5, name: "The Eyre Affair">,
#<Book id: 28, author_id: 3, initial_version: 2, version: 5, name: "Something Rotten">,
#<Book id: 29, author_id: 3, initial_version: 3, version: 4, name: "Lost in a Good Book">]
jasper.books
=> []