Uniqueness validations in Rails

Validations on models in Rails are very powerful. They allow you to express certain properties that attributes of your model must possess, like uniqueness, numericality or presence, and Rails automatically takes care of enforcing them when a model is persisted. Moreover, Rails also provides customizable messages that tell you what went wrong when certain validations didn’t pass.

The uniqueness validator ensures that rows in your table are unique for certain columns. For instance, you can use it to ensure that your users’ emails are unique by setting a uniqueness validator on the attribute email of your Users table. However, because validations are not enforced at the database level, things can go wrong when your application consists of multiple processes. The following diagram makes this obvious.

Consistency failure for uniqueness validations in Rails

From http://robots.thoughtbot.com/post/55689359336/the-perils-of-uniqueness-validations

The uniqueness validation is not atomic, therefore we can a typical synchronization issue that arises when you have concurrent processes.

Unique indexes on database

The way to solve this problem is to declare unique indexes on the unique column. This way, the second creation in the diagram would fail because of the constraint on the database. This new situation is shown in the diagram below.

Uniqueness validations are consistent with database indexes.

From http://robots.thoughtbot.com/post/55689359336/the-perils-of-uniqueness-validations

Using Rails migrations, you can declare an index as follows.

class AddEmailIndexToUser
  def change
    # If you already have non-unique index on email, you will need
    # to remove it before you're able to add the unique index.
    add_index :users, :email, unique: true
  end
end

The consistency_fail gem and Git

The consistency_fail gem analyses your models and identifies situations where you declared a uniqueness validator on an attribute without a corresponding database index. In order to identify such cases early on, I run the gem before each commit using a pre-commit hook in Git. This forces me to fix the problem because I am confronted with it each time that I make a commit and the commit fails if consistency_fail identifies a problem.

My .git/hooks/pre-commit file looks like this:

#!/bin/sh

# Invoke consistency_fail for uniqueness index failures
consistency_fail

Make sure it is executable.

chmod +x .git/hooks/pre-commit
Write us your thoughts about this post. Be kind & Play nice.