Uniqueness validation does not work since the beginning of Ruby on Rails.
Tags: ruby, rails, programming, webdev
Published: 2023-02-04 17:08:22

This story is about a problem of uniqueness validation in [ActiveRecord ](https://github.com/rails/rails/tree/main/activerecord)([Ruby on Rails](https://github.com/rails/rails) default [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)). Even though the issue is quite common, most available guidelines provide incomplete solutions. That is why I want to share how to deal with the problem and ensure it does not come back.

Feel free to skip to the last section if you know the topic well. I believe you will find helpful information there.

## What is wrong?

Before answering the question, let us recall how the validation works. Assume we have the following model.

```ruby
class User < ActiveRecord::Base
  validates :email, uniqueness: true
end
```

Every method call that triggers validations will query a database to know if the given email is already taken.

```sql
SELECT 1 FROM users WHERE email = ?
```

In real-life applications, multiple running instances may process users’ requests simultaneously. That means two `SELECT` queries for the same email may run in parallel and result in passed validations. This issue is called “[race condition](https://en.wikipedia.org/wiki/Race_condition),” which can break your model’s expectations.

We can break the model’s expectations even easier.

```ruby
# given array of User
users

# Run the validations on every User instance
# It also saves the state for the sequential calls
if users.all?(&:valid?)
  # No validation will be triggered
  users.each(&:save!)
else
  p “Something goes wrong”
end
```

Or, you may skip validations in some cases, therefore accidentally breaking the expectations.

```ruby
user.save!(validate: false)
```

Now you understand the problem and probably know the solution, but stay with me for a little longer; you won’t regret it.

## Incomplete solution

The ultimate solution that most guides speak about is adding a database constraint, a unique index to be precise, to ensure database consistency.

You can achieve that with the following migration.

```ruby
class AddUsersEmailUniqueIndex < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end
```

It will be executed then as the following SQL statement.

```sql
CREATE UNIQUE INDEX index_users_on_email ON users(email);
```

By having this constraint, you are safe from breaking your model’s expectations by either of the described situations in the section above.

So far, we have briefly discussed what is provided in many sources. So let me explain what is wrong with this solution.

## You have to remember about the index!

We are all human-being. We must remember every detail, especially during hard work toward achieving the product’s goals.

There multiple things to remember to keep things consistent:

- adding a new unique validation means adding a new unique index
- removing means removing both
- adjusting means aligning both

Understandably, this is very error-prone.

## It does not work out of the box!

One day your team decided to add a unique validation to an existing model widely used across the application. Gladly, you remembered to cover the unique validation with the corresponding unique index.

Unfortunately, as soon as you released the new validation on production, your team started getting bugs with 500 errors. After a short investigation, you find that all of them are `ActiveRecord::RecordNotUnique`.

Now you might correctly assume that the race conditioning is happening on the production, but still, why it fails with 500 errors instead of 422 errors like it would with any other validation?

And the answer is the need for more support from ActiveRecord to recognize the database error and map it to the validation error if it matches. To avoid that, you must add `rescue` blocks to your application in appropriate places and do the mapping manually. Another way would be to change the calls with “safe” methods such as `create_or_find_by!()`.

Postpone this decision until you read the final section of this article.

Full solution

The critical issues I mentioned early are solved by these two open-source gems (_both libraries can do even more!_):

- [DatabaseConsistency ](https://github.com/djezzzl/database_consistency)— finds inconsistent indexes and validations
- [DatabaseValidations ](https://github.com/toptal/database_validations)— turns unique index violations into validation errors

[DatabaseConsistency ](https://github.com/djezzzl/database_consistency)is a tool to avoid various issues due to inconsistencies and inefficiencies between a database schema and application models.

Run the simple command below, and it will find inconsistencies for you. The following output is for missing unique indexes.

```bash
$ bundle exec database_consistency
MissingUniqueIndexChecker fail User email model should have proper unique index in the database
```

The following one is for cases when an index is present, but the corresponding unique validation does not cover it.

```bash
$ bundle exec database_consistency
UniqueIndexChecker fail User index_users_on_email index is unique in the database but do not have uniqueness validator
```

[DatabaseValidations ](https://github.com/toptal/database_validations)provides database-driven validations for ActiveRecord.

In our case, we should update the model to specify db_uniqueness instead of uniqueness

```ruby
class User < ActiveRecord::Base
  validates :email, db_uniqueness: true
end
```

With this simple change, it will:

- ensure during run-time that the appropriate unique index covers the validation in the database
- seamlessly transform `ActiveRecord::RecordNotUnique` errors to validation errors (turns 500 errors to 422)

That is it about pitfalls of uniqueness validations in ActiveRecord.

_**I hope the article was helpful for you. Subscribe for the upcoming topics**_:

- [LinkedIn](https://www.linkedin.com/in/evgeniydemin/)
- [Twitter](https://twitter.com/EvgeniyDemin/)
- [GitHub](https://github.com/djezzzl)
- [DevTO](https://dev.to/evgeniydemin)