Creating Unique Constraint/Index Ecto.Migration: Elixir

Alvin Rapada
2 min readDec 11, 2020

Have you ever found yourself in a position where you can find anything about something you really need? Just happen to encounter this problem doing my task for a company I’m working with and I can’t find proper documentation about it. So here it is, hope I'm the last one who had troubles trying and finding the right way to do this.

Migration

Let's start with the migration, say you want to create a movie table with two fields (title and genre) and you don't want to create entries with duplicate values of those fields.

create table(:movies) do
add(:genre, :string)
add(:title, :string)
timestamps()
end
create(
unique_index(
:movies,
~w(title genre)a,
name: :index_for_movies_duplicate_entries
)
)

index_for_movies_duplicate_entries: can be any name you want, can also be automatically generated based on the fields included on your constraint.

~w(title genre)a: a list of fields to checks for a unique constraint. Can also be [:title, :genre], I just like the sigil one.

Schema

Make sure to pipe the unique_constraint/3 on your changeset.

defmodule Data.Schemas.Movie do
use Data.Schema
schema "movies" do
field(:genre, :string)
field(:title, :string)
timestamps()
end
@required ~w(title)a
@optional ~w(genre)a
def changeset(struct, params \\ %{}) do
struct
|> cast(params, @optional ++ @required)
|> validate_required(@required)
|> unique_constraint(
:title,
name: :index_for_movies_duplicate_entries
)

end
end

The 2nd parameter of unique_constraint/3 can be any field that’s a part of the constraint, in this case, the title.

Test

To test this unique constraint, you just need to insert two entries with the same values.

defmodule Data.Schemas.MovieTest do
use Data.SchemaCase
alias Data.Schemas.Movie

test "testing changeset with unique contraints" do
params = %{
title: "Taken 4",
genre: "action"
}

%Movie{}
|> Movie.changeset(params)
|> Repo.insert()

m2 = %Movie{}
|> Movie.changeset(params)

{:error, changeset} = Repo.insert(m2)
refute changeset.valid?
end
end

The changeset should not be valid, hence the refute changeset.valid? and return this error

got: #Ecto.Changeset<action: :insert, 
changes: %{title: "Taken 4", genre: "action"}, 6157
errors: [value: {"has already been taken", []}],
data: #Data.Schemas.Movie<>,
valid?: false>

And there you have it. hope this can help. 🎉

If you find this helpful, I’d appreciate a coffee 😊

--

--