Ecto.Changeset.unique_constraint
unique_constraint
, go back to Ecto.Changeset module for more information.
Specs
Checks for a unique constraint in the given field or list of fields.
The unique constraint works by relying on the database to check if the unique constraint has been violated or not and, if so, Ecto converts it into a changeset error.
In order to use the uniqueness constraint, the first step is to define the unique index in a migration:
create unique_index(:users, [:email])
Now that a constraint exists, when modifying users, we could annotate the changeset with a unique constraint so Ecto knows how to convert it into an error message:
cast(user, params, [:email])
|> unique_constraint(:email)
Now, when invoking Repo.insert/2
or Repo.update/2
, if the
email already exists, it will be converted into an error and
{:error, changeset}
returned by the repository. Note that the error
will occur only after hitting the database so it will not be visible
until all other validations pass.
Options
:message
- the message in case the constraint check fails, defaults to "has already been taken":name
- the constraint name. By default, the constraint name is inferred from the table + field(s). May be required explicitly for complex cases:match
- how the changeset constraint name is matched against the repo constraint, may be:exact
or:suffix
. Defaults to:exact
.:suffix
matches any repo constraint whichends_with?
:name
to this changeset constraint.
Complex constraints
Because the constraint logic is in the database, we can leverage all the database functionality when defining them. For example, let's suppose the e-mails are scoped by company id:
# In migration
create unique_index(:users, [:email, :company_id])
# In the changeset function
cast(user, params, [:email])
|> unique_constraint([:email, :company_id])
The first field name, :email
in this case, will be used as the error
key to the changeset errors keyword list. For example, the above
unique_constraint/3
would generate something like:
Repo.insert!(%User{email: "john@elixir.org", company_id: 1})
changeset = User.changeset(%User{}, %{email: "john@elixir.org", company_id: 1})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [email: {"has already been taken", []}]
In complex cases, instead of relying on name inference, it may be best to set the constraint name explicitly:
# In the migration
create unique_index(:users, [:email, :company_id], name: :users_email_company_id_index)
# In the changeset function
cast(user, params, [:email])
|> unique_constraint(:email, name: :users_email_company_id_index)
Partitioning
If your table is partitioned, then your unique index might look different per partition, e.g. Postgres adds p<number> to the middle of your key, like:
users_p0_email_key
users_p1_email_key
...
users_p99_email_key
In this case you can use the name and suffix options together to match on these dynamic indexes, like:
cast(user, params, [:email])
|> unique_constraint(:email, name: :email_key, match: :suffix)
Case sensitivity
Unfortunately, different databases provide different guarantees
when it comes to case-sensitiveness. For example, in MySQL, comparisons
are case-insensitive by default. In Postgres, users can define case
insensitive column by using the :citext
type/extension. In your migration:
execute "CREATE EXTENSION IF NOT EXISTS citext"
create table(:users) do
...
add :email, :citext
...
end
If for some reason your database does not support case insensitive columns, you can explicitly downcase values before inserting/updating them:
cast(data, params, [:email])
|> update_change(:email, &String.downcase/1)
|> unique_constraint(:email)