Bringing sanity to ActiveRecord validations

ActiveRecord uniqueness validations cannot be used on their own - we must use database level constraints to guard against race conditions. Given that we are guarding against duplication at the database level, should we also introduce uniqueness constraints in ActiveRecord models, and if so, under what circumstances?

All of the examples use the locations table. The code column in this table has a unique index. Let’s start off by adding the following ActiveRecord validation to our model:

class Location < ActiveRecord::Base
  validates_uniqueness_of :code

When we save a change to an existing record, we see something like the following:

Save with uniqueness check

Note the presence of Location Exists. This extra database call to ensure code is unique occurs before every update, even when code was not one of the columns updated! The performance for this extra database call is somewhat severe. 1000 updates, average of 10 runs produced:

For those keeping score, that’s 55% slower.

Let’s say we never change code in our application - we could change the validation to validates_uniqueness_of :code, on: :create so we only see the extra database call once during a record’s lifespan. We could also use ActiveModel::Dirty to run the validation only when code has changed with validates_uniqueness_of :code, if: :code_changed?.

We’ve demonstrated model level uniqueness validations can be expensive, so why bother when we can rescue from ActiveRecord::RecordNotUnique?

As Erik Michaels-Ober noted in his presentation at Baruco 2014, Writing Fast Ruby, using exceptions for control flow in Ruby is over 10x slower than if/else. Benchmarking 1000 failed creates, (also average of 10 runs) produced:

In addition to the 10% speed increase, the ActiveRecord validation also has the advantage of making it possible to see all validation errors at once in model.errors. A RecordNotUnique exception is only thrown when we hit the database so the user would never see a uniquness-related error if their inputs failed any other validation, leaving them with the frustrating experience of making corrections in two steps instead of one.