Hi, I’ve been playing around with something I call
acts_as_multi_versioned. The difference from acts_as_versioned is that
you can work with different versions simultaneously. For example if
one session sees one version, and another session sees another. It’s
designed like this:
All tables need to have two primary-keys, id (autoincrement) and
version (not autoincrement). There is no extra table for older
versions, everything is in the same table.
The variable ActiveRecord::Base.current_version is used to define what
version use currently use. That version will be used for all tables.
When an object, for example with id 3, don’t exists for version 4, the
system will search for the closest version available (which means
max(version < current_version). So, a new version only takes extra
space for the modified objects.
Currently it looks like obj.destroy is like doing revert on a revision
system :). Needs to be fixed. Currently it’s just a proof of concept,
but it would be nice with comments and suggestions.
Have a nice day!
Tobias Nurmiranta
Minimal example (the function ver sets the current version):
Loading development environment.
n = Node.create :name => “aaa”
=> #<Node:0xb6fe42d0 @errors=#<ActiveRecord::Errors:0xb6fb5a20
@errors={}, @base=#<Node:0xb6fe42d0 …>>, @new_record=false,
@attributes={“name”=>“aaa”, “id”=>22, “version”=>0}>n.name
=> “aaa”ver 1
=> 1n.name = “bbb”
=> “bbb”n.save
=> truever 0
=> 0Node.find(22).name
=> “aaa”ver 1
=> 1Node.find(22).name
=> “bbb”ver 25
=> 25Node.find(22)
=> #<Node:0xb7378a2c @attributes={“name”=>“bbb”, “id”=>“22”,
“version”=>“1”}>
Monkey-patch:
require ‘composite_primary_keys’
class ActiveRecord::Base
class << self
attr_accessor :current_version
end
self.current_version = 0
def self.acts_as_multi_versioned
class_eval %q{
set_primary_keys :id, :version
class << self
def find_with_version(obj, *args)
case obj
when :first, :all
find_without_version(obj, *args)
else
begin
find_without_version(obj,
ActiveRecord::Base.current_version, *args)
rescue => e
find_without_version(:all, (args[0] || {}).merge(
{:order => ‘version desc’, :limit => 1, :conditions
=> [‘id = ? AND version < ?’, obj,
ActiveRecord::Base.current_version]})).first || (raise e)
end
end
end
alias_method_chain :find, :version
end
def before_create
self[:version] = ActiveRecord::Base.current_version
end
def update_without_callbacks_with_version
if self.version != ActiveRecord::Base.current_version
self[:version] = ActiveRecord::Base.current_version
# must create a new object when storing a new version?
# but the statement above makes the old record store into
the new row?
o = self.clone
o[:id] = self[:id]
o.save!
else
update_without_callbacks_without_version
end
end
alias_method_chain :update_without_callbacks, :version
# Note: needed monkey-patch to be able to use id as one of the
primary keys.
def id
self[:id]
end
def id=(val)
self[:id] = val
end
# Creates a new record with values matching those of the
instance attributes.
# Note: call self.ids instead of self.ids
def create_without_callbacks
unless self.ids; raise CompositeKeyError, “Composite keys do
not generated ids from sequences, you must provide id values”; end
attributes_minus_pks = attributes_with_quotes(false)
cols = quoted_column_names(attributes_minus_pks) <<
self.class.primary_key
vals = attributes_minus_pks.values << quoted_id
self[:id] = connection.insert(
"INSERT INTO #{self.class.table_name} " +
"(#{cols.join(', ')}) " +
"VALUES(#{vals.join(', ')})",
"#{self.class.name} Create"
)
@new_record = false
return true
end
}
end
end
setting current version…
def ver(i)
ActiveRecord::Base.current_version = i
end