Lately, I’ve been learning Ruby on Rails. In layman’s terms, Ruby on Rails is a system for making dynamic websites like Flickr, Friendster, and yes, Music For Dozens. To drastically oversimplify, some of the smart people have gone ahead and handled all of the common tasks like logging on, handling user input from forms, remembering data and displaying it when appropriate, etc. The idea is to make it easier to create a dynamic website by letting you focus just on the unique thing that you want your site to do and not all the boring and difficult infrastructure all such sites need.
Chris and I are writing a super-secret exciting new MFDZ project in Rails and, eventually, we’ll be rewriting MFDZ itself in it. In the process, I’ve come across some opportunities to actually get paid for working on other people’s Rails projects, so I’ve been devoting some semi-serious time to improving my skills. Predictably, I’ve encountered a few beginner-type problems to which I couldn’t find great solutions online. Some of these I’ve been able to work through with the online help of some of the many strong Rails programmers in Portland and a few others I managed to solve with just my own wits. I figured it would be the neighborly thing to do to write down some of what I’ve learned here for the sake of the next poor soul who would otherwise Google-up empty on the same problem.
Thus, I introduce learns_to, a new category of post here at IDFDZ where I’ll try to document a little bit of what I’ve learned about Rails as clearly and concretely as I can. If you’re one of the people who emails me about my blog becoming too boring and “technical” or, more specifically, you’re just not interested in Rails, I urge you to move on now. But, if you’re me from earlier this week — wondering how you’ll ever get acts_as_versioned to work with your project when you barely know the difference between a plugin and a gem — then hopefully you’ve come to just the right place.
What it is
Acts_as_versioned is a chunk of Rails code meant to help you keep track of, well, progressive versions of whatever it is your application keeps track of. The quintessential example is the way a wiki keeps track of edits made to a page’s information. In fact, acts_as_versioned is great for giving wiki-like revisible user-edited content to any part of your Rails app.
Where to get it</h4
As I alluded to in the introduction, there are two different ways to get your hands on acts_as_versioned: as a plugin or as a gem. RubyGems is Ruby‘s built-in packaging system. It has a number of conveniences including easy command line installation and updating. These characteristics make it definitely the best way to get Rails itself and to keep your local copy fresh. However, in the context of an add-on like acts_as_versioned, I would recommend using plugins whenever you have the opportunity. Plugins have the great advantage that they are automatically included by Rails itself when you start the server. You don’t have to write any fancy setup code. That means not having to venture into environment.rb, or any other exotic places that can be scary for Rails newbies like me.
So, go ahead and download acts_as_versioned now (that link is to a .tgz, just what you’ll need on the mac). Unzip it and move it into the /vendor/plugins folder in your Rails app and we’ll get started using it.
Database setup
Before we get too far into the workings here, I should point out the documentation. It is not especially human readable, but it is a definitive reference for this stuff (most of what I’m about to say, I figured out by staring at the documentation for the Class Methods for a long time).
Anyway, the first thing we’ve got to do is get our system setup to use acts_as_versioned. This means database setup and some small changes to our models. Let’s say we’re doing a music app. We’ve got artists and we’re representing them in an ‘artists’ table and a corresponding Artist class. Our artists have names and bios and, of course, ids. That means that our migration for creating our table looks like this (pre-versioning):
create_table :artists do |t|
t.column :name, :string
t.column :bio, :text
end
(If you’re not familiar with migrations, they’re the system Rails provides for representing your database structure and, especially, changes you make to it in code. They are super convenient. The Understanding Migrations tutorial on the Rails wiki is a great place to get started with them and, if you have any further questions, the Rails migration documentation is comprehensive. The sooner you start using migrations, the sooner you’ll fall in love with Rails. Note: When you use migrations to set up your database, Rails adds ids automatically wherever they belong, which is why I didn’t specify one here.)
Now, acts_as_versioned is going to mirror our artist data into a parallel table called ‘artist_versions’. Each row of that table will represent a subsequent state, or version, of each of our artists. So we need to create the artist_versions table with a version column, a foreign key to tell it which artist its keeping track of (in this case we’ll call that one ‘artist_id’), and a column for each bit of data we want to version from our original model; for now, let’s just do bio. All this adds up to a migration that looks like this:
Artist.create_versioned_table do |t|
t.column :version, :integer
t.column :artist_id, :integer
t.column :bio, :text
end
So the obvious thing to point out here is that we’re using a new method “create_versioned_table” and that it’s a method on the Artist model itself. In order for this to work, then, we’re going to have to tell our Artist model that something’s going on with versions. It’s super easy; just one line: ‘acts_as_versioned’ within the Artist class. I like to keep it up near the relation declarations at the top so I don’t lose track of it. Once we add that, the plugin does the rest of the work, adding a whole boatload of methods to our model including this migration method, create_versioned_table (and the converse we’ll use in the down part of our migration: “Artist.drop_versioned_table”). Again, if you’re doing this by hand, remember that our migration automatically adds an id to our table.
Assuming we’ve combined these two bits together into a migration and run it, we’ll now have our database properly in place and we can start using all the methods the acts_as_versioned plugin has added to our Artist model.
Using the methods provided by acts_as_versioned
As you’ll soon learn if you spend some time with the documentation, acts_as_versioned provides methods to do most things you can think of with it. I’m only going to go into detail here on the two that I think are most useful: revert_to, for rolling back to previous versions of your model and find_versions which is great for displaying old states of your data.
Let’s start with find_versions. If @artist is an instance variable containing a particular artist then in our view we can do something like:
<% for version in @artist.find_versions.reverse %>
Version <%= version.version %> <br />
<%= link_to '(revert to this version)',
:action => 'revert_to_version',
:version_id => version.id,
:artist_id => @artist %>
<% end %>
This view code iterates over all the saved versions of our artist (starting with the most recent and heading backwards) displaying the version number and then providing a link to revert to that version. Other similar methods will let you find specific versions that meet given criteria or to get at just the particular attributes that you’re keeping versioned.
Now, let’s look closer at that link_to call. It’s calling a custom controller action called ‘revert_to_version’, passing in the id of the version we want to revert to and the id of our artist. We want this link to revert the artist we’ve got stored in @artist to the version whose id we’re passing in. The controller code necessary to do this will use the revert_to method provided by acts_as_versioned, like so:
def revert_to_version
@artist = Artist.find( params[:artist_id] )
@artist.revert_to! params[:version_id]
redirect_to :action => 'show', :id => @artist
end
All we’re doing here grabbing ahold of our artist instance using the normal class ‘find’ method. Then we just call revert_to! (we use the conventional exclamation mark syntax to save as well as reverting) with the version_id as an argument and the old version of the artist’s bio will now be saved in the right place in the artists table. One nice thing about using acts_as_versioned in this way is that it is non-destructive; all the more recent versions since the one to which we just reverted are still saved in our artist_versions table and we can always un-revert to them (if that’s not too confusing).
And that is pretty much an introduction to acts_as_versioned. I’ve just scratched the surface of the subtle things you can do with it, especially when it comes on setting conditions for saving new versions. I’ve said it twice before, but a third time couldn’t hurt: read the documentation. There’s lots to learn.
Many thanks to Rick Olson, the author of acts_as_versioned, both for his great plugin and his rapid, clear, and helpful support when I was first trying to use it myself.
Tagged: rails, ruby, acts_as_versioned, wiki, plugin, gem, tutorial
Thank you for this article—I found it very helpful! Now I’m looking forward to more learns_to posts…
No problem, Sarah. I’m glad it helped!
Hey Greg, do you know if it’s possible to use acts_as_versioned with join tables? The most important information in my system lives in the join tables, but since they don’t have models defined for them I’m worried the approach you outline won’t work for them.
Your best bet for using acts_as_versioned with join tables would probably be to take advantage of one of the new features in Rails 1.1.: the through relationship. The way it works is that you associate a model with your join like so:
class Author
has_and_belongs_to_many :books, :through => :authorship
end
And now you’ve got a model, Authorship, on which you can hang attributes and special properties of that relationship (like acts_as_versioned) as well as all of the relevant methods.
Scott’s Place has a good tutorial on getting started using :through.
You can start using this behaviour right now by upgrading to Edge Rails — where most of the 1.1. features are already up and running.
There’s one additional piece to this, I just discovered. In addition to what I had you do above, you’ve got to set the association on the versioned model. See Rails Weenie’s tip for more detail.
Thank you. This article was quite usefull and I’m also now a huge advocate of Migrations.
No problem, Jeff. Glad I could help. If you’re interest, I also wrote up something of an introduction to migrations a little while after writing this. If this post got your curiousity up or you’re otherwise just getting started with migrations, you might find it helpful.
Thanks for your help, Greg, I’ll be looking into :through.
Hi there, i’m not sure if its just me, but I seem to have found an error in your code, i’ve spent the last few days trying to figure out why only one of my pages (of a wiki) would revert while the others would not.
Where you’ve created the link_to, instead of passing the version.id it should be the version.version as this is needed to revert to that version number not the id.
My problem was that the one page that would revert happened to be one of the first versions saved in the database so its version.id was 1 and version.version 1 also, this was the same for its version.id 2 but not version.id 3.
So the first 2 versions of the page were reverting but not the 3rd as its version.id was diff from its version.version.
Phew. Hope this helps someone, tell me what you think.
Thank you for this write up – it was very helpful. I do have a question that I was hoping you could help answer… Is there anything special that needs to be set up when using acts_as_versioned and the model to be versioned has a lock_version field? Thank you in advance.
hi ,
I am working on a project which already has tables named versions , languages_versions etc..
and it has version, version_id all over the place … 🙁
is there a way to make sure that there is no name conflict at all ?? because the tables named ‘products’ etc are working with acts_as_versioned but the ones like versions etc 🙁
i tried using
acts_as_versioned
:table_name => :_backup_pversions ,
:version_column => :_backup
and created tables with these names.. still not working..
is it possible to use acts_as_versioned for my project without renaming versions to something else in the entire code…????
hi ,
[plz ignore the previous mesg of mine ]
I am working on a project which already has tables named versions , languages_versions etc..
and it has version, version_id all over the place … 🙁
is there a way to make sure that there is no name conflict at all ?? because the tables named ‘products’ etc are working with acts_as_versioned but not the ones like versions etc 🙁
i tried using
acts_as_versioned
:table_name => :_backup_versions ,
:version_column => :_backup
and created tables with these names.. still not working..
is it possible to use acts_as_versioned for my project without renaming versions to something else in the entire code…????
Thanks for a great tutorial!
your link to Rails Weenie is no longer working. Could you provide the missing info somewhere in this post?
Many thanks
What about when you’re adding attributes to a pre-existing, versioned model? Seems like you have to manually add the column to both the model and version table, which isn’t very DRY. Or is there a special way to do this?
Great post! Really helped get started. But what about the ‘find_version’ method? I got the impression that I could use that method on an instance variable(as per http://ar-versioned.rubyforge.org/classes/ActiveRecord/Acts/Versioned/ActMethods.html#M000004)
Am I mistaken in this assumption?
@Jonas: http://www.railsforum.com/viewtopic.php?pid=41534