Validating Uniqueness in Ecto 2.2

elixir ecto

Posted

As the changelog shows, Ecto 2.2 will support validating uniqueness using unsafe_validate_unique/3. This is kind of a big change for Ecto, and since I helped bring it about, I thought it might be worth explaining.

Here's a quick recap of how we got here.

The ActiveRecord Way

A lot of ORMs and querying libraries support validating uniqueness. Ruby's ActiveRecord is one that I'm familiar with.

class User < ApplicationRecord
  validates :username, uniqueness: true
end

Before inserting a user, this model would query for existing users with that address, bail if it finds any and proceed if it doesn't.

This is inherently unsafe, as I've written about before, because a race condition could lead to two users with the same username. Rubyists generally use a unique index in the database to give real assurance. They typically don't bother rescuing the error that results if it's violated (though I've written about a way to do that).

The Ecto Way

Recognizing that application-level validations can't prevent data conflicts, the authors of Ecto initially decided not to support them.

Instead, the strategy was:

The Downside of Constraints

This works well. But it's not always a great user experience, because unlike validations, constraints are checked one-at-a-time. In other words, if your data violates 3 constraints, the database will only tell you about the first one it finds.

So in the worst case, the user experience might be:

Not awesome.

The "Belt and Suspenders" Approach

With the addition of unsafe_validate_unique/3, you can now check uniqueness in the validation phase. (The third argument is the repo.)

There are some downsides to this.

So in the vast majority of cases, you'll be able to fix "username is taken" at the same time as "name can't be blank", without a separate form submission.

But it's unsafe, as the name indicates. You still need the database constraint, and you you should still use unique_constraint(changeset, :username) if you want users to get a friendly message in the rare case of a race condition.

Using them both may feel redundant, but if you want to give users the best experience you can and still guarantee that your data is correct, it's the way to go as of Ecto 2.2.

Clarification: if you check uniqueness as the user types via JS requests to the server, the user is already getting the fastest feedback possible, and you shouldn't need any other check except the uniqueness constraint.