Although optimistic locking is a feature that has been in Rails for a long time, I find that I and those around me rarely take advantage of it. While you can easily get started with optimistic locking just by looking at the Rails API Docs, you'll quickly find that you need to do more then add a
lock_version column to take full advantage of this feature.
The use case for optimistic locking is preventing users from overwriting changes made by other users. Let's say Billy comes to your awesome travel web app to make a change to a location, but just before he submits, Jenny comes and submits a change. Optimistic locking will prevent Billy's changes from going through. Getting started with optimistic locking is really as simple as adding a
lock_version column to every table on which you want locking enabled.
After adding this column, every update to a model will result in this lock version being incremented. If for some reason you can't use the column name
lock_version, no problem: use some other name and just set that in the class like so:
Once you've done this, if two people try to submit an update to a model at the same time, one of them will cause an
ActiveRecord::StaleObjectError error to be raised.
Fixing the edit
While this behavior is helpful, it doesn't solve a more worrisome problem. Let's say Billy comes to change that same vacation destination. He opens up the edit form, bangs away at the content and walks away to get some coffee. He gets distracted and doesn't come back to work on it for a few hours. While he's away, Jenny makes a quick change to the destination. He comes back and finishes the content changes and submits the edit form. What happens? Well, with optimistic locking out of the box, his changes succeed and go through to the database. This is problematic for me, and is not what I would expect. This happens because the
lock_version is set from the database when you instantiate the model object inside the update action. What we need is the model to be locked for Billy to the version he has when he accesses the edit form. The best way to accomplish this is by adding a hidden input for the
lock_version field. Then, when someone submits the form, if the lock version has been incremented since they accessed it, the update fails with an
ActiveRecord::StaleObjectError error. You can do this by hand, by adding this hidden field to every form you need locking on, like so:
Alternatively, you can make your life easier and just add this code to your application:
This code modifies the
form_for helper to automatically add a
lock_version hidden input for every object that has locking enabled and isn't new (new records aren't versioned yet, and submitting a lock version causes problems). With this in place you don't need to remember to add the lock version to every form you need it on. Great! Now Billy can get coffee for as long as he wants and not have to worry about overwriting Jenny's changes. I have seen some other suggestions out there to tie the form to the lock version when the user accesses the edit form, like putting the model object in session, or the lock version in session. All of these solutions have their flaws, though. Storing a model in session is a bad idea for a number of reason that have been explained well elsewhere. Overall, I believe storing the lock version on the form is the best way to handle this type of locking, as it ensures that the lock version is tied to the form the user has in front of them.
Out with the Stale in with the New
A remaining problem is that, if Billy has made this change after Jenny already made a change, an
ActiveRecord::StaleObjectError error is raised which by default results in a blank page. The simplest solution to this is to add a static HTML error page at
public/409.html. What I prefer to do is catch the error and render the edit page with a flash error message telling the user what happened. We also want to ensure the record: 1) is the latest version, 2) isn't using the lock version from the original edit, and 3) is using the attribute values the user entered on the form. The reason for all of this is to ensure the form has the correct lock version and has the values the user supplied to make it easy for them to resubmit the form. You can do that right in the controller action like this:
As your application grows, you'll want to DRY up multiple
rescue ActiveRecord::StaleObjectError blocks with something like this:
You'll also notice in the
rescue_from block both XML and JSON API requests are handled by just returning a 409 “Conflict” status code with no body.
Optimistic locking isn't right for every situation; some applications won't need any type of locking, and others may need more strict forms of locking. You may also find that you need to recover from conflicts differently. For instance, you might want to show the current version in the database next to the version the user is trying to submit. However, if you feel it covers most of your applications' use cases, optimistic locking is so easy to implement there's no reason you shouldn't try it.
About Viget Labs In 1999, Viget Labs started building web products for startups. Since 2005, they've been building them in Ruby on Rails. Now, Viget Labs' team of nearly 50 works with both startups and big brands from their offices near Washington, DC, in Durham, NC, and Boulder, CO.