# Acts\_as\_multi\_versioned

**URL:** <https://www.ruby-forum.com/t/acts-as-multi-versioned/106812>\
**Category:** Rails\
**Created:** [June 14, 2007, 10:50am UTC](https://www.ruby-forum.com/t/acts-as-multi-versioned/106812 "2007-06-14T10:50:52Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Tobias\_Numiranta](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/4491bb/32.png) [@Tobias\_Numiranta](https://www.ruby-forum.com/u/Tobias_Numiranta)\
**Post date:** [June 14, 2007, 10:50am UTC](https://www.ruby-forum.com/t/acts-as-multi-versioned/106812/1 "2007-06-14T10:50:52Z")

</div>

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  
> > =\> 1
> 
> > n.name = “bbb”  
> > =\> “bbb”
> 
> > n.save  
> > =\> true
> 
> > ver 0  
> > =\> 0
> 
> > Node.find(22).name  
> > =\> “aaa”
> 
> > ver 1  
> > =\> 1
> 
> > Node.find(22).name  
> > =\> “bbb”
> 
> > ver 25  
> > =\> 25
> 
> > Node.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

## class Node \< ActiveRecord::Base acts\_as\_multi\_versioned end

---

<div class="post-metadata">

**Author:** ![Tobias\_Numiranta](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/t/4491bb/32.png) [@Tobias\_Numiranta](https://www.ruby-forum.com/u/Tobias_Numiranta)\
**Post date:** [July 25, 2007, 9:59am UTC](https://www.ruby-forum.com/t/acts-as-multi-versioned/106812/2 "2007-07-25T09:59:15Z")

</div>

Hi. I’ve updated the code mentioned below, if anyone is interested or  
want to comment. Tables with version-control now have the extra  
columns version (primary key) and version\_deleted (boolean indicating  
if the object is deleted in this version). I needed some complicated  
SQL to get it working with find(:all) and find(:first) :). More  
details in the old mail.

Have a nice day!  
Tobias

– code –

# acts\_as\_multi\_versioned

class ActiveRecord::Base  
class \<\< self  
attr\_accessor :current\_version  
end  
self.current\_version = 0

def self.acts\_as\_multi\_versioned  
class\_eval %q{  
class \<\< self  
def find\_version(obj, version, args = {})  
# Adding a join-statement which selects the rows with  
highest  
# version =\< current\_version for every unique value of id.  
args[:joins] = “INNER JOIN (SELECT  
#{primary\_key},MAX(version) as version FROM #{table\_name} WHERE  
version \< #{version+1} GROUP BY #{primary\_key}) #{table\_name}\_versions  
ON #{table\_name}\_versions.#{primary\_key} =  
#{table\_name}.#{primary\_key} AND #{table\_name}\_versions.version =  
#{table\_name}.version AND #{table\_name}.version\_deleted = 0  
#{args[:joins]}”  
args[:readonly] ||= false  
find\_without\_version(obj, args)  
end  
def find\_with\_version(obj, args = {})  
find\_version(obj, ActiveRecord::Base.current\_version, args)  
end  
alias\_method\_chain :find, :version  
end

```
  def before_create
    self[:version] = ActiveRecord::Base.current_version
  end

  def update_without_callbacks
    if self.version != ActiveRecord::Base.current_version
      self[:version] = ActiveRecord::Base.current_version
      # Must create a new object when storing a new version-

```

number. But the  
# old record will work correctly for the next update.  
o = self.clone  
o[self.class.primary\_key] = self[self.class.primary\_key] #  
why is this needed?  
o.save!  
else  
# Modified update\_without\_callbacks from  
composite\_primary\_keys.  
where\_class = “(#{self.class.primary\_key} = #{quoted\_id})  
AND (version = #{self[:version]})”

```
      connection.update(

        "UPDATE #{self.class.table_name} " +

        "SET #{quoted_comma_pair_list(connection,

```

attributes\_with\_quotes\_versioned)} " +

```
        "WHERE #{where_class}",

        "#{self.class.name} Update"

      )

      return true
    end
  end

  def destroy_without_callbacks
    begin
      self.class.find_version(self.id, self.version - 1) # older

```

version exists?  
self.version\_deleted = true  
self.save! # (this will clone and create a deleted record if  
only readaccess was made earlier)

```
    rescue ActiveRecord::RecordNotFound # older record doesn't

```

exist, remove it from database.  
where\_class = “(#{self.class.primary\_key} = #{quoted\_id})  
AND (version = #{self[:version]})”  
unless new\_record?

```
        connection.delete(

          "DELETE FROM #{self.class.table_name} " +

          "WHERE #{where_class}",

          "#{self.class.name} Destroy"

        )

      end
    end
    freeze
  end

  def attributes_with_quotes_versioned # All attributes except

```

primary-key and ‘version’.  
x = attributes\_with\_quotes(false)  
x.delete “version”  
x  
end  
}  
end  
end

ActiveRecord::Base.current\_version = 3
