2 min read

ActiveRecord Optimistic Locking

ActiveRecord Optimistic Locking

Optimistic locking is an alternative to pessimistic locking except that it sort of "builds in" locking mechanisms to an entire table and its corresponding ActiveRecord model. In pessimistic locking, you have to manually call with_lock on an ActiveRecord model while in optimistic locking, you introduce a column called lock_version in your database table, which automatically enables optimistic locking.

The lock_version column will be incremented every time a change is committed to the record in question. Thus, if there are two processes accessing the same record, and one process makes an update to the record, the second process won't be able to modify the record unless it re-retrieves the newly updated data. This can prevent problems that can come with concurrent access to the same data.

This is pretty much straight from the Rails docs but I can't think of any better way to explain some example usages of optimistic locking, so here goes.

Example 1: Optimistic locking preventing two instances of the same record overriding each other.

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.first_name = "should fail"
p2.save # Raises an ActiveRecord::StaleObjectError

Example 2: Optimistic locking preventing deletion of the same record when lock_version is out of date.

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.destroy # Raises an ActiveRecord::StaleObjectError

To add optimistic locking to an ActiveRecord model, add the column lock_version to your database table with a datatype of integer. Something like this.

class AddLockingToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :lock_version, :integer, default: 0, null: false
  end
end

lock_version is the default column name that Rails will look for, but you can also customize the column name by overriding an attribute called locking_column within the ActiveRecord model like this.

class User < ActiveRecord::Base
  self.locking_column = :custom_locking_column_name
end

Personally, I prefer pessimistic locking since I can choose specific instances where I want to lock records. To me, optimistic locking seems akin to the evil (in my mind) default_scope that override default scoping behaviors in ActiveRecord.

I would say optimistic locking is a toolbox that one can grab for, but only do so when there's a really good reason to do it as it completely overrides out-of-box default ActiveRecord behavior.