Ā¶ ā
Vestal Versions<img src=āhttps://badge.fury.io/rb/vestal_versions.pngā alt=āGem Versionā /> <img src=āhttps://travis-ci.org/laserlemon/vestal_versions.png?branch=masterā alt=āBuild Statusā /> <img src=āhttps://codeclimate.com/github/laserlemon/vestal_versions.pngā alt=āCode Climateā /> <img src=āhttps://coveralls.io/repos/laserlemon/vestal_versions/badge.pngā alt=āCoverage Statusā /> <img src=āhttps://gemnasium.com/laserlemon/vestal_versions.pngā alt=āDependency Statusā />
Finally, DRY ActiveRecord versioning!
acts_as_versioned
[http://github.com/technoweenie/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
[http://github.com/laserlemon/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!
Ā¶ ā
CompatibilityTested with Active Record 3.2.16 with Ruby 1.9.3 and 1.9.2.
Ā¶ ā
InstallationIn the Gemfile:
** Note: I am giving this project some much needed love to keep her relevant in a post Rails 3 world. I will be finalizing a version to support 1.9.2+ and Rails 3.2+ soon and pushing the gem, till then, use the git repo: ~dreamr
gem 'vestal_versions', :git => 'git://github.com/laserlemon/vestal_versions'
Next, generate and run the first and last versioning migration youāll ever need:
$ rails generate vestal_versions:migration $ rake db:migrate
Ā¶ ā
ExampleTo 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
Ā¶ ā
Upgrading to 1.0For the most part, version 1.0 of vestal_versions
is backwards compatible, with just a few notable changes:
-
The versions table has been beefed up. Youāll need to add the following columns (and indexes, if you feel so inclined):
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
-
When a model is created (or updated the first time after being versioned), an initial version record with a number of 1 is no longer created. These arenāt used during reversion and so they end up just being dead weight. Feel free to scrap all your versions where
number == 1
after the upgrade if youād like to free up some room in your database (but you donāt have to). -
Models that have no version records in the database will return a
@user.version
of 1. In the past, this would have returnednil
instead. -
Version
has moved toVestalVersions::Version
to make way for custom version classes. -
Version#version
did not survive the move toVestalVersions::Version#version
. That alias was dropped (too confusing). UseVestalVersions::Version#number
.
Ā¶ ā
New to 1.0There 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
:
-
The ability to completely skip versioning within a new
skip_version
block:@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
andappend_version
blocks. Themerge_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. Theappend_version
block works similarly except that the resulting single version is combined with the most recent version in the history and saved. -
Version tagging. Any version can have a tag attached to it (must be unique within the scope of the versioned parent) and that tag can be used for reversion.
@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.
-
Resetting. This is basically a hard revert. The new
reset_to!
instance method behaves just like therevert_to!
method except that after the reversion, it will also scrap all the versions that came after that target version.@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
-
Storing which user is responsible for a revision. Rather than introduce a lot of controller magic to guess what to store, you can simply update an additional attribute on your versioned model:
updated_by
.@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">
-
Global configuration. The new
vestal_versions
Rails generator also writes an initializer with instructions on how to set application-wide options for theversioned
method. -
Conditional version creation. The
versioned
method now accepts:if
and:unless
options. Each expects a symbol representing an instance method or a proc that will be evaluated to determine whether or not to create a new version after an update. An array containing any combination of symbols and procs can also be given.class User < ActiveRecord::Base versioned :if => :really_create_a_version? end
-
Custom version classes. By passing a
:class_name
option to theversioned
method, you can specify your own ActiveRecord version model.VestalVersions::Version
is the default, but feel free to stray from that. I recommend that your custom model inherit fromVestalVersions::Version
, but thatās up to you! -
A
versioned?
convenience class method. If your user model is versioned,User.versioned?
will let you know. -
Soft Deletes & Restoration. By setting
:dependent
to:tracking
destroys will be tracked. On destroy a new version will be created storing the complete details of the object with a tag of ādeletedā. The object can later be restored using therestore!
method on the VestalVersions::Version record. The attributes of the restored object will be set using the attribute writer methods. After a restore! is performed the version record with the ādeletedā tag is removed from the history.class User < ActiveRecord::Base versioned :dependent => :tracking end >> @user.version => 2 >> @user.destroy => <User id: 2, first_name: "Steve", last_name: "Jobs", ... > >> User.find(2) => ActiveRecord::RecordNotFound: Couldn't find User with ID=2 >> VestalVersions::Version.last => <VestalVersions::Version id: 4, versioned_id: 2, versioned_type: "User", user_id: nil, user_type: nil, user_name: nil, modifications: {"created_at"=>Sun Aug 01 18:39:57 UTC 2010, "updated_at"=>Sun Aug 01 18:42:28 UTC 2010, "id"=>2, "last_name"=>"Jobs", "first_name"=>"Stephen"}, number: 3, tag: "deleted", created_at: "2010-08-01 18:42:43", updated_at: "2010-08-01 18:42:43"> >> VestalVersions::Version.last.restore! => <User id: 2, first_name => "Steven", last_name: "Jobs", ... > >> @user = User.find(2) => <User id: 2, first_name => "Steven", last_name: "Jobs", ... > >> @user.version => 2
Ā¶ ā
Thanks!Thank you to all those who post issues and suggestions. And special thanks to:
-
splattael, who first bugged (and helped) me to write some tests for this thing
-
snaury, who helped out early on with the
between
association method, the:dependent
option and a conflict from using a method calledchanges
-
sthapit, who was responsible for the
:only
and:except
options as well as showing me that Iām a dummy for storing a useless first version
To contribute to vestal_versions
, please fork, hack away in the integration branch and send me a pull request. Remember your tests!