Hello everyone, I just wanted to get some opinions on this extension I''ve been working on based on an idea by courtenay. acts_as_versioned is a simple class method that saves versions of models into a separate table. Here''s how it works: Model.create_revisions_table # creates revisions table. do this in the console or in a migration class Model < ActiveRecord::Base acts_as_versioned end Now, all changes will be logged to model_revisions. A timestamp will always be assigned. If you need to rollback, just call revert_to: @model.title = ''hello world'' @model.save @model.revision #=> 1 @model.title = ''hello world!'' @model.save @model.revision #=> 2 @model.revisions.size #=> 2 @model.revert_to(1) @model.title #=> ''hello world'' @model.revision #=> 1 @model.revisions.size #=> 2 As you can see, models are given a has_many association to its revisions. The revision attribute is not required, but shows the current revision id if available. The IDs probably won''t be in sequential order like they were in my perfect example. I''ve thought about some configuration options too, such as the revisions table name, the revision property in the model table, how many revisions to store, and finally, some requirements or validations on the revision model. For instance, maybe I wouldn''t want to save a revision if the creator_id is identical and the updated_at is less than 5 minutes ago. The source is available at http://techno-weenie.net/svn/projects/rails_ext/active_record/acts/revisions.rb if anyone would like to take a look. For now, just throw this in your lib/ directory and put this in environment.rb to use: require_dependency ''revisions'' ActiveRecord::Base.class_eval { include ActiveRecord::Acts::Revisions } -- rick http://techno-weenie.net
Hi Rick, I think this is a great idea. I have no time to test it right away as I am working on some other stuff right now. But I had implemented versioning myself. It would be great if this would be part of Rails in the future. One thing that comes to mind is that I wanted to be able to only only save a version if the modified object had been changed. I quickly read through your code and couldn''t see that you are doing that. All in all "great stuff". Thanks Sascha Ebach
Rick Olson wrote:>Hello everyone, I just wanted to get some opinions on this extension >I''ve been working on based on an idea by courtenay. acts_as_versioned >is a simple class method that saves versions of models into a separate >table. > >Cool, that''s really useful! I''ll be making lots of use of revisions in a current project so that''s brilliant timing. Can you put it up on the wiki somewhere so I can link to it? -- R.Livsey http://livsey.org
> Cool, that''s really useful! I''ll be making lots of use of revisions in a > current project so that''s brilliant timing. > > Can you put it up on the wiki somewhere so I can link to it?Well, I''m working on a patch for it at the moment. Once I get that done, I''ll make a wiki entry of the rdoc comments. Does anyone have any bright ideas on what to do if I encounter a ''type'' field? Should I save it in the versioned table as versioned_type? The problem is, @model.versions will return an array of versions of whatever type is in the type column, instead of ModelVersion. I encountered this while righting the unit tests. -- rick http://techno-weenie.net
On Saturday 16 July 2005 22:30, Rick Olson wrote:> Hello everyone, I just wanted to get some opinions on this extension > I''ve been working on based on an idea by courtenay. > acts_as_versioned is a simple class method that saves versions of > models into a separate table.For inspiration have a look at the temporal patterns written down by Martin Fowler http://www.martinfowler.com/ap2/timeNarrative.html The PLoPD 4 papers Fowler refers to can be found on the web: http://jerry.cs.uiuc.edu/~plop/plop98/final_submissions/P04.pdf http://jerry.cs.uiuc.edu/~plop/plop98/final_submissions/P09.pdf http://jerry.cs.uiuc.edu/~plop/plop98/final_submissions/P63.pdf http://www.manfred-lange.com/publications/TimeTravel.pdf Michael -- Michael Schuerig This is not a false alarm mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org This is not a test http://www.schuerig.de/michael/ --Rush, Red Tide
> For inspiration have a look at the temporal patterns written down by > Martin Fowler > > http://www.martinfowler.com/ap2/timeNarrative.html > > The PLoPD 4 papers Fowler refers to can be found on the web: > > http://jerry.cs.uiuc.edu/~plop/plop98/final_submissions/P04.pdf > http://jerry.cs.uiuc.edu/~plop/plop98/final_submissions/P09.pdf > http://jerry.cs.uiuc.edu/~plop/plop98/final_submissions/P63.pdf > http://www.manfred-lange.com/publications/TimeTravel.pdfAwesome. I''ll have to read that more indepth later... I went ahead and fleshed it out. Added configurations, added some API docs, and added some ActiveRecord tests for postgresql and mysql. A research patch has been created at http://dev.rubyonrails.org/ticket/1758. I did some tweaks to the API. Since I went with the name acts_as_versioned, I changed all the method names to match that. Here''s the wiki page as requested: http://wiki.rubyonrails.com/rails/show/ActsAsVersioned. For now it''s only available at at http://techno-weenie.net/svn/projects/rails_ext/active_record/acts/versioned.rb unless you feel comfortable applying research patch. -- rick http://techno-weenie.net
Rick Olson wrote:> I did some tweaks to the API. Since I went with the name > acts_as_versioned, I changed all the method names to match that.Very nice, forgot to mention that. Better than revision.> Here''s the wiki page as requested: > http://wiki.rubyonrails.com/rails/show/ActsAsVersioned. For now it''s > only available at at > http://techno-weenie.net/svn/projects/rails_ext/active_record/acts/versioned.rb > unless you feel comfortable applying research patch.Now, if only this would work: page = Page.create(:title => ''hello world!'') page.version # => 1 page.title = ''hello world!'' # notice, nothing changed page.version # => 1 Sometimes a user could save an object, for example a Page, and nothing has changed, so no additional version should be created. Sascha Ebach
> Sometimes a user could save an object, for example a Page, and nothing > has changed, so no additional version should be created.That goes back to one of the areas I''d like to expand on: requirements. I''d really like a clean way to pass some rules to the Version model specifying when a new version is saved. Maybe in your wiki you only want a new version if the author_id is different or the time since the last revision is greater than 5 minutes, for example. Any ideas on how this should look are welcome. I just want the acts_as_versioned syntax to look as natural as possible. The other area is setting how many versions to keep, in case you didn''t want the versioned table to hold every versioned item. -- rick http://techno-weenie.net
Yes, this really take it to another level. Only thinking about it for a second or two I suppose it is best to just do it like the other acts. Maybe this for the number of versions to hold: acts_as_versioned :max_versions => 20 For the condition when a version is saved I would propose to only save it if atleast one of the fields has changed. def after_find @original = self.attributes.dup end Naturally, you would loop through the attributes on before_update. "Maybe in your wiki you only want a new version if the author_id is different or the time since the last revision is greater than 5 minutes" To expand on the idea above: acts_as_versioned :max_versions => 20, :conditions => :update_conditions def update_conditions @original.author != self.attributes[''author''] # untested end Sascha
> acts_as_versioned :max_versions => 20 > > For the condition when a version is saved I would propose to only save > it if atleast one of the fields has changed. > > > def after_find > @original = self.attributes.dup > end > > Naturally, you would loop through the attributes on before_update. > > "Maybe in your wiki you only want a new version if the author_id is > different or the time since the last revision is greater than 5 minutes" > > To expand on the idea above: > > acts_as_versioned :max_versions => 20, :conditions => :update_conditions > > def update_conditions > @original.author != self.attributes[''author''] # untested > endYou know what, for some reason I was thinking that it should be validated on the Version model, but just passing a symbol to a :conditions option is much simpler. I''ll add that and the max_versions options to the patch. I''m a little worried about using after_find after the performance issues. Should I not worry? I was thinking that I would just pull the most recent version and compare with that. It''s probably better to only be penalized in performance when you''re actually updating, rather than everytime you''re pulling records from the DB. -- rick http://techno-weenie.net
> You know what, for some reason I was thinking that it should be > validated on the Version model, but just passing a symbol to a > :conditions option is much simpler. I''ll add that and the > max_versions options to the patch.Great :)> I''m a little worried about using after_find after the performance > issues. Should I not worry? I was thinking that I would just pull > the most recent version and compare with that. It''s probably better > to only be penalized in performance when you''re actually updating, > rather than everytime you''re pulling records from the DB.Hmm, you might be right. I haven''t thought of that before. That is a good idea. Sascha
FWIW, in my applications that have called for versioned content, I have used a slightly different pattern: I have one table called "revisions", with columns "table_name", "record_id", and "content" (plus a bit more metadata) -- and the "content" field simply holds a marshalled version of an object. That way, I can add versioning to any of my models without creating a new table for each, and I don''t have to maintain duplicate schemas. I haven''t done any benchmarking, but in my application the version-control functions aren''t used heavily, so the marshalling/unmarshalling overhead isn''t a bottleneck. IMHO, it would be great if some future acts_as_versioned extension could allow for this single-table/marshalled-objects approach, when it''s called for. :sco
Scott Raymond wrote:> FWIW, in my applications that have called for versioned content, I > have used a slightly different pattern: I have one table called > "revisions", with columns "table_name", "record_id", and "content" > (plus a bit more metadata) -- and the "content" field simply holds a > marshalled version of an object. > > That way, I can add versioning to any of my models without creating a > new table for each, and I don''t have to maintain duplicate schemas. I > haven''t done any benchmarking, but in my application the > version-control functions aren''t used heavily, so the > marshalling/unmarshalling overhead isn''t a bottleneck. > > IMHO, it would be great if some future acts_as_versioned extension > could allow for this single-table/marshalled-objects approach, when > it''s called for.+1 That is a great idea. It is probably not the right solution for everything, but as a simple version store it has its advantages. You can easily use compression on the data and save space. You kinda loose searchability, but it would be nice to have this as a second option. Sascha
+2!! Great idea. On Jul 16, 2005, at 7:57 PM, Richard Livsey wrote:> Rick Olson wrote: > >> Hello everyone, I just wanted to get some opinions on this extension >> I''ve been working on based on an idea by courtenay. acts_as_versioned >> is a simple class method that saves versions of models into a separate >> table. > Cool, that''s really useful! I''ll be making lots of use of revisions in > a current project so that''s brilliant timing. > > Can you put it up on the wiki somewhere so I can link to it? > > -- > R.Livsey > http://livsey.org > > > _______________________________________________ > Rails mailing list > Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org > http://lists.rubyonrails.org/mailman/listinfo/rails > >___________________ Ben Jackson Diretor de Desenvolvimento +55 (21) 9997-0593 ben-p14LI7ZcAE/pVLaUnt/cCQC/G2K4zDHf@public.gmane.org http://www.incomumdesign.com
> > IMHO, it would be great if some future acts_as_versioned extension > > could allow for this single-table/marshalled-objects approach, when > > it''s called for.Sounds great. I''d like to get the other options (requirements and limits) added before I start tinkering with this. I''d really like to keep the act as ''rails like'' as possible, so here''s what I was thinking: acts_as_versioned :table_layout => :individual || :global # :individual being the current behavior It would be nice to use separate acts for them, but I can''t really think of a good name. acts_as_globally_versioned? I''ll have to do some research, perhaps these patterns have good names that can be used. Feel free to post patches to the ticket (http://dev.rubyonrails.org/ticket/1758) if you''d like to help. Test cases are always welcome. Also, I''ve only tested on mysql and postgresql so far. -- rick http://techno-weenie.net
Rick Olson wrote:>>>IMHO, it would be great if some future acts_as_versioned extension >>>could allow for this single-table/marshalled-objects approach, when >>>it''s called for. > > > Sounds great. I''d like to get the other options (requirements and > limits) added before I start tinkering with this. I''d really like to > keep the act as ''rails like'' as possible, so here''s what I was > thinking: > > acts_as_versioned :table_layout => :individual || :global # > :individual being the current behaviorwhat about: acts_as_versioned :store => :individual || :marshal # :individual being the current behavior instead of marshal, I could think of "simple"> It would be nice to use separate acts for them, but I can''t really > think of a good name. acts_as_globally_versioned? I''ll have to do > some research, perhaps these patterns have good names that can be > used.I have thought about that, too, but I don''t think this is a good idea. Essentially, what you are doing, is simply exchanging the data store for the versioned objects. I don''t think this deserves a seperate name.> Feel free to post patches to the ticket > (http://dev.rubyonrails.org/ticket/1758) if you''d like to help. Test > cases are always welcome. Also, I''ve only tested on mysql and > postgresql so far.I would love to, but client work comes first :(. I wish I could come back to Rails, couldn''t work on my CMS for 3 months now. Sascha
> Sounds great. I''d like to get the other options (requirements and > limits) added before I start tinkering with this. I''d really like to > keep the act as ''rails like'' as possible, so here''s what I was > thinking: > > acts_as_versioned :table_layout => :individual || :global # > :individual being the current behavior[...] The rails way is to come up with an convention for and then proving tools to aid in this approach. This is the 90 / 10 rule applied to functionality vs effort. With 10% of the work you can cover 90% of the cases. In this case the _revisions table is a great convention. Its extremely efficient and doesn''t clutter the sql up at all ( if you keep everything in one table you have to do some major sql lifting ).
Tobias wrote:> In this case the _revisions table is a great convention. Its extremely > efficient and doesn''t clutter the sql up at all ( if you keep > everything in one table you have to do some major sql lifting ).Chalk it up to aesthetic differences, but I find the one-table approach to be simpler both conceptually and in terms of "SQL clutter". I''d much rather have one "revisions" table and use an STI-like approach to store different types of objects in it, rather than having users_revisions, articles_revisions, events_revisions, payments_revisions... etc etc. Plus, when I change my table schema down the line, I don''t have to mirror my changes to a "_revisions" table -- which allows my "historical" data to remain untainted, with only the attributes that existed at the time it was frozen. When I want to add simple versioning to a model, I can just add a few lines like this: class Post < ActiveRecord::Base has_many :revisions, :foreign_key => ''record_id'', :conditions => "table_name=''posts''" def after_update revisions.create :table_name => table_name, :content => Marshal.dump(self) end end ...and my Revision.rb looks like... class Revision < ActiveRecord::Base def record; Marshal.restore(content); end end Obviously that''s just bare-bones functionality, but to my eye, it''s anything but cluttered. :sco blinksale.com
> Obviously that''s just bare-bones functionality, but to my eye, it''s > anything but cluttered.+1
On 19/07/2005, at 2:00 PM, Caleb Buxton wrote:>> Obviously that''s just bare-bones functionality, but to my eye, it''s >> anything but cluttered. > > +1(+1)++
This is a great little patch, which I''ve only just got around to applying, as an experiment on a current project. As a suggestion for an enhancement, I was thinking about the possibility of not tying the columns n the _versions table to those in the original table. This would mean that if a column doesn''t appear in the _versions table, it doesn''t get versioned. As I understand from my tests so far, you need to have the same columns in both tables. If anyone thinks this is a good idea, I may apply the changes myself in the next few days. Something else I''d also like to see is dependency versioning. For example, if a Page model has_many Attributes, then when the Attributes are updated, the Page version is updated, as are the Attributes, and the whole version update is tied together somehow (needs a little more thought). -Phil On 17 Jul 2005, at 05:44, Rick Olson wrote:> I did some tweaks to the API. Since I went with the name > acts_as_versioned, I changed all the method names to match that. > Here''s the wiki page as requested: > http://wiki.rubyonrails.com/rails/show/ActsAsVersioned. For now it''s > only available at at > http://techno-weenie.net/svn/projects/rails_ext/active_record/acts/ > versioned.rb > unless you feel comfortable applying research patch._______________________________________________ Rails mailing list Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org http://lists.rubyonrails.org/mailman/listinfo/rails
FWIW, I think I like Scott''s approach the best. Only problem that I could forsee is that restoring a marshalled object would be problematic if the schema was updated. This would potentially invalidate the revisions. Is my thinking just incorrect, or are there any good ways to approach this problem? On 7/18/05, Scott Raymond <scottraymond-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org> wrote:> Tobias wrote: > > In this case the _revisions table is a great convention. Its extremely > > efficient and doesn''t clutter the sql up at all ( if you keep > > everything in one table you have to do some major sql lifting ). > > Chalk it up to aesthetic differences, but I find the one-table > approach to be simpler both conceptually and in terms of "SQL > clutter". I''d much rather have one "revisions" table and use an > STI-like approach to store different types of objects in it, rather > than having users_revisions, articles_revisions, events_revisions, > payments_revisions... etc etc. > > Plus, when I change my table schema down the line, I don''t have to > mirror my changes to a "_revisions" table -- which allows my > "historical" data to remain untainted, with only the attributes that > existed at the time it was frozen. > > When I want to add simple versioning to a model, I can just add a few > lines like this: > > class Post < ActiveRecord::Base > has_many :revisions, :foreign_key => ''record_id'', :conditions => > "table_name=''posts''" > def after_update > revisions.create :table_name => table_name, :content => Marshal.dump(self) > end > end > > ...and my Revision.rb looks like... > > class Revision < ActiveRecord::Base > def record; Marshal.restore(content); end > end > > Obviously that''s just bare-bones functionality, but to my eye, it''s > anything but cluttered. > > :sco > blinksale.com > _______________________________________________ > Rails mailing list > Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org > http://lists.rubyonrails.org/mailman/listinfo/rails >