Path: | README.rdoc |
Last Update: | Sun Feb 13 15:42:39 +0000 2011 |
Finally, DRY ActiveRecord versioning!
acts_as_versioned by technoweenie was a great start, but it failed to keep up with ActiveRecord‘s introduction of dirty objects in version 2.1. Additionally, each versioned model needs its own versions table that duplicates most of the original table‘s columns. The versions table is then populated with records that often duplicate most of the original record‘s attributes. All in all, not very DRY.
vestal_versions requires only one versions table (polymorphically associated with its parent models) and no changes whatsoever to existing tables. But it goes one step DRYer by storing a serialized hash of only the models’ changes. Think modern version control systems. By traversing the record of changes, the models can be reverted to any point in time.
And that‘s just what vestal_versions does. Not only can a model be reverted to a previous version number but also to a date or time!
In environment.rb:
Rails::Initializer.run do |config| ... config.gem 'vestal_versions' ... end
At your application root, run:
$ sudo rake gems:install
Next, generate and run the first and last versioning migration you‘ll ever need:
$ script/generate vestal_versions $ rake db:migrate
To version an ActiveRecord model, simply add versioned to your class like so:
class User < ActiveRecord::Base versioned validates_presence_of :first_name, :last_name def name "#{first_name} #{last_name}" end end
It‘s that easy! Now watch it in action…
>> u = User.create(:first_name => "Steve", :last_name => "Richert") => #<User first_name: "Steve", last_name: "Richert"> >> u.version => 1 >> u.update_attribute(:first_name, "Stephen") => true >> u.name => "Stephen Richert" >> u.version => 2 >> u.revert_to(10.seconds.ago) => 1 >> u.name => "Steve Richert" >> u.version => 1 >> u.save => true >> u.version => 3 >> u.update_attribute(:last_name, "Jobs") => true >> u.name => "Steve Jobs" >> u.version => 4 >> u.revert_to!(2) => true >> u.name => "Stephen Richert" >> u.version => 5
For the most part, version 1.0 of vestal_versions is backwards compatible, with just a few notable changes:
change_table :versions do |t| t.belongs_to :user, :polymorphic => true t.string :user_name t.string :tag end change_table :versions do |t| t.index [:user_id, :user_type] t.index :user_name t.index :tag end
There are a handful of exciting new additions in version 1.0 of vestal_versions. A lot has changed in the code: much better documentation, more modular organization of features, and a more exhaustive test suite. But there are also a number of new features that are available in this release of vestal_versions:
@user.version # => 1 @user.skip_version do @user.update_attribute(:first_name, "Stephen") @user.first_name = "Steve" @user.save @user.update_attributes(:last_name => "Jobs") end @user.version # => 1
Also available, are merge_version and append_version blocks. The merge_version block will compile the possibly multiple versions that would result from the updates inside the block into one summary version. The single resulting version is then tacked onto the version history as usual. The append_version block works similarly except that the resulting single version is combined with the most recent version in the history and saved.
@user.name # => "Steve Richert" @user.update_attribute(:last_name, "Jobs") @user.name # => "Steve Jobs" @user.tag_version("apple") @user.update_attribute(:last_name, "Richert") @user.name # => "Steve Richert" @user.revert_to("apple") @user.name # => "Steve Jobs"
So if you‘re not big on version numbers, you could just tag your versions and avoid the numbers altogether.
@user.name # => "Steve Richert" @user.version # => 1 @user.versions.count # => 0 @user.update_attribute(:last_name, "Jobs") @user.name # => "Steve Jobs" @user.version # => 2 @user.versions.count # => 1 @user.reset_to!(1) @user.name # => "Steve Richert" @user.version # => 1 @user.versions.count # => 0
@user.update_attributes(:last_name => "Jobs", :updated_by => "Tyler") @user.versions.last.user # => "Tyler"
Instead of passing a simple string to the updated_by setter, you can pass a model instance, such as an ActiveRecord user or administrator. The association will be saved polymorphically alongside the version.
@user.update_attributes(:last_name => "Jobs", :updated_by => current_user) @user.versions.last.user # => #<User first_name: "Steven", last_name: "Tyler">
class User < ActiveRecord::Base versioned :if => :really_create_a_version? end
Thank you to all those who post issues and suggestions. And special thanks to:
To contribute to vestal_versions, please fork, hack away in the integration branch and send me a pull request. Remember your tests!