Ruby on Rails
ActsAsVersioned (Version #83)

Description

An experimental feature by technoweenie (see link at bottom). Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version column is present as well.

Database Schema

The model that you’re versioning needs to have a ‘version’ attribute. The model is versioned into a table called #{model}_versions where the model name is singular. The _versions table should contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.

Acts_as_versioned comes prepared with the create_versioned_table method, perfect for a migration:

class AddPostsVersions < ActiveRecord::Migration
  require_dependency 'post'
  def self.up
    # create_versioned_table takes the same options hash
    # that create_table does
    Post.create_versioned_table
  end

  def self.down
    Post.drop_versioned_table
  end
end

Example


class Page < ActiveRecord::Base
  # assumes page_versions table
  acts_as_versioned
end

page = Page.create(:title => 'hello world!')
page.version       # => 1

page.title = 'goodbye world'
page.save # doesn't this have to be saved before the version changes?
page.version       # => 2
page.versions.size # => 2

page.revert_to(1)  # using version number
page.title         # => 'hello world!'

page.revert_to(page.versions.last) # using versioned instance
page.title         # => 'goodbye world'

page.revert_to!(1) # reverts and saves the model's changes

Configuration options are:

  • class_name – versioned model class name (default: \PageVersion in the above example)
  • table_name – versioned model table name (default: pages_versions in the above example)
  • foreign_key – foreign key used to relate the versioned model to the original model (default: page_id in the above example)
  • inheritance_column – name of the column to save the model’s type value for SingleTableInheritance. (default: versioned_type)
  • version_column – name of the column in the model that keeps the version number (default: version, and ignored if not found). This should be set to :lock_version if you want the optimistic locking column to pull double duty and handle the versioning sequence.
  • sequence_name – name of the custom sequence to be used by the versioned model.
  • limit – number of revisions to keep, defaults to unlimited
  • if – symbol of method to check before saving a new version. If this method returns false, a new version is not saved. For finer control, pass either a Proc or modify Model#version_condition_met?
    
      acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
    

    or…
    
      class Auction
        def version_condition_met? # totally bypasses the <tt>:if</tt> option
          !expired?
        end
      end
    
  • if_changed – Simple way of specifying attributes that are required to be changed before saving a model. This takes either a symbol or array of symbols.
    h2. Availability

Provided as a Rails plugin. Install by first updating your plugin source list if necessary, then installing the plugin:

$ ruby script/plugin discover
$ ruby script/plugin install acts_as_versioned

Migrating to ActsAsVersioned

I had been using my own versioning system, keeping old versions in the same table as current versions in a sort of linked list. I managed to convert this to ActsAsVersioned using the following in a migration (very ugly):


  # versions is an ordered array of all old versions, oldest first
  # primary is the current version and is included in the versions array
  # the tables and models have already been set to act as versioned
  versions.each do |obsolete_rev|
    # See ActsAsVersioned set_new_version; ignores locking?
    primary.send("#{primary.class.version_column}=", (primary.versions.calculate(:max, :version) || 0) + 1)
    primary.save_without_revision!
    # See ActsAsVersioned save_version_on_create
    rev = primary.class.versioned_class.new
    primary.clone_versioned_model(obsolete_rev, rev)
    rev.version = primary.send(primary.class.version_column)
    rev.send("#{primary.class.versioned_foreign_key}=", primary.id)
    rev.save
    obsolete_rev.destroy unless obsolete_rev == primary
  end

—Patrick

Comments

  • Apparently there has to be a field in the x_versions table for every field in the original ‘x’ table (not just the fields you want versioned, as the docs here say)
    > You can configure this by setting in the Model:
    non_versioned_columns << ‘name_of_non_versioned_field’
  • This seems a very cool idea for versioning, but you anyone help improve this page? maybe with some more examples?
  • Has anybody done any work with versioning foreign key relationships such that Something has_many :others can make a new version of the something when its others collection changes?
  • create_versioned_table can add version column to model’s table automatically if there is no column named version or lock_version. drop_versioned_table won’t remove this column from model.
  • There is a performance bug, possibly only on Edge Rails (I’m running rev. 5712), that causes Rails to load the entire #{model}_versions table from the DB each time a #{model} object is saved. This affects the ActsAsVersioned plugin since versions are associated to the #{model}, and may get very large if updated frequently. See Rails ticket 6822. canadaduane

Ejemplo de uso, en español
http://ljorquera.blogspot.com/2007/08/versionado-de-tablas-con-ruby-on-rails.html

Description

An experimental feature by technoweenie (see link at bottom). Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version column is present as well.

Database Schema

The model that you’re versioning needs to have a ‘version’ attribute. The model is versioned into a table called #{model}_versions where the model name is singular. The _versions table should contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.

Acts_as_versioned comes prepared with the create_versioned_table method, perfect for a migration:

class AddPostsVersions < ActiveRecord::Migration
  require_dependency 'post'
  def self.up
    # create_versioned_table takes the same options hash
    # that create_table does
    Post.create_versioned_table
  end

  def self.down
    Post.drop_versioned_table
  end
end

Example


class Page < ActiveRecord::Base
  # assumes page_versions table
  acts_as_versioned
end

page = Page.create(:title => 'hello world!')
page.version       # => 1

page.title = 'goodbye world'
page.save # doesn't this have to be saved before the version changes?
page.version       # => 2
page.versions.size # => 2

page.revert_to(1)  # using version number
page.title         # => 'hello world!'

page.revert_to(page.versions.last) # using versioned instance
page.title         # => 'goodbye world'

page.revert_to!(1) # reverts and saves the model's changes

Configuration options are:

  • class_name – versioned model class name (default: \PageVersion in the above example)
  • table_name – versioned model table name (default: pages_versions in the above example)
  • foreign_key – foreign key used to relate the versioned model to the original model (default: page_id in the above example)
  • inheritance_column – name of the column to save the model’s type value for SingleTableInheritance. (default: versioned_type)
  • version_column – name of the column in the model that keeps the version number (default: version, and ignored if not found). This should be set to :lock_version if you want the optimistic locking column to pull double duty and handle the versioning sequence.
  • sequence_name – name of the custom sequence to be used by the versioned model.
  • limit – number of revisions to keep, defaults to unlimited
  • if – symbol of method to check before saving a new version. If this method returns false, a new version is not saved. For finer control, pass either a Proc or modify Model#version_condition_met?
    
      acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
    

    or…
    
      class Auction
        def version_condition_met? # totally bypasses the <tt>:if</tt> option
          !expired?
        end
      end
    
  • if_changed – Simple way of specifying attributes that are required to be changed before saving a model. This takes either a symbol or array of symbols.
    h2. Availability

Provided as a Rails plugin. Install by first updating your plugin source list if necessary, then installing the plugin:

$ ruby script/plugin discover
$ ruby script/plugin install acts_as_versioned

Migrating to ActsAsVersioned

I had been using my own versioning system, keeping old versions in the same table as current versions in a sort of linked list. I managed to convert this to ActsAsVersioned using the following in a migration (very ugly):


  # versions is an ordered array of all old versions, oldest first
  # primary is the current version and is included in the versions array
  # the tables and models have already been set to act as versioned
  versions.each do |obsolete_rev|
    # See ActsAsVersioned set_new_version; ignores locking?
    primary.send("#{primary.class.version_column}=", (primary.versions.calculate(:max, :version) || 0) + 1)
    primary.save_without_revision!
    # See ActsAsVersioned save_version_on_create
    rev = primary.class.versioned_class.new
    primary.clone_versioned_model(obsolete_rev, rev)
    rev.version = primary.send(primary.class.version_column)
    rev.send("#{primary.class.versioned_foreign_key}=", primary.id)
    rev.save
    obsolete_rev.destroy unless obsolete_rev == primary
  end

—Patrick

Comments

  • Apparently there has to be a field in the x_versions table for every field in the original ‘x’ table (not just the fields you want versioned, as the docs here say)
    > You can configure this by setting in the Model:
    non_versioned_columns << ‘name_of_non_versioned_field’
  • This seems a very cool idea for versioning, but you anyone help improve this page? maybe with some more examples?
  • Has anybody done any work with versioning foreign key relationships such that Something has_many :others can make a new version of the something when its others collection changes?
  • create_versioned_table can add version column to model’s table automatically if there is no column named version or lock_version. drop_versioned_table won’t remove this column from model.
  • There is a performance bug, possibly only on Edge Rails (I’m running rev. 5712), that causes Rails to load the entire #{model}_versions table from the DB each time a #{model} object is saved. This affects the ActsAsVersioned plugin since versions are associated to the #{model}, and may get very large if updated frequently. See Rails ticket 6822. canadaduane

Ejemplo de uso, en español
http://ljorquera.blogspot.com/2007/08/versionado-de-tablas-con-ruby-on-rails.html