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
On Jun 14, 10:50 am, Tobias Numiranta
<nurmira...-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
wrote:> 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
> -----
--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups
"Ruby on Rails: Talk" group.
To post to this group, send email to
rubyonrails-talk-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org
To unsubscribe from this group, send email to
rubyonrails-talk-unsubscribe-/JYPxA39Uh5TLH3MbocFFw@public.gmane.org
For more options, visit this group at
http://groups.google.com/group/rubyonrails-talk?hl=en
-~----------~----~----~----~------~----~------~--~---