Ecto changesets help us cast, validate, filter, and manipulate data. This post will walk through how to leverage Ecto changesets to validate data before we insert it into the database to ensure data integrity. For example, let’s say that in our app, we can create a user with a name, but we want to validate the name before we insert it into the database.
Before diving into what Ecto changesets can do to validate our data,
let’s first look at a barebones create action in a Phoenix controller and see how
changesets play a role in our controller. Since we only expect the user to enter a name,
the user_params
will look like {name: "Michael Jordan"}
.
At a very high level, the code above passes the user_params
into the changeset and
then tries to insert the changeset into the database. Depending on the changeset,
the insert will either be successful with a {:ok, _}
and we return a 200 status
or the insert will not be successful {:error, _}
and we return a 422 status
.
Out of the box, the changeset function will simply cast the data to prepare it to be inserted into the database. This is what a simple changeset function inside the User model looks like:
We pass in the %User{}
as our model
and user_params
as our params
.
We cast the name
field and the changeset function will return:
Note how the constraints
and errors
are empty! This means the
the changes are valid. We will have to add code to our changeset
so that if the changes are not valid, the changeset will tell us in constraints
or errors
.
Adding A Built-In Constraint
Let’s say that we want to add a constraint that the name needs to be unique. We cannot have two users with the same name because that will be confusing. Luckily, Ecto has built in constraint that we can take advantage of.
Below, we are adding a unique constraint on the name field into the changeset:
And if we now try to insert a new user with the name Michael Jordan
, the changeset
will look like this:
Aha! The changeset now includes constraints: [%{constraint: "users_name_index", field: :name,
message: "has already been taken", type: :unique}
. Now, when we do Repo.insert(changeset, conn: conn)
,
it will not insert the data and instead will return {:error, _}
.
Adding a Custom Constraint
We can also build our own validations if the ones that Ecto provides out of the box
are insufficient. For example, let’s say that we want to validate the uniqueness of
all names, but for some reason, we do not want to validate the uniquess of name if the name is Michael Jordan
.
We are okay with as many users as possible with the name Michael Jordan
, but only Michael Jordan
(I know, a silly example :smile:).
In the above example, we include a new private function special_validate_unique_name
in the changeset and pass the changeset into it. Inside the function, we get the name from the changeset
and see if the name is Michael Jordan
. If the name is Michael Jordan
, then we do
nothing and pass the changeset through. If the name is not Michael Jordan
, we take
the changeset and add a unique constraint to it for the name.
It works!
Adding a Validation
We have only been looking at constraints, but we can also add built in and custom
validations to our changeset. For example, let’s say that we also want to make sure
that our name field does not go above 20 characters. We can add Ecto’s built in
validate_length
validation.
Trying to insert a user with a name longer than 20 characters with the changeset function will yield the following changeset:
Just like when the changesets includes constraints
, when the changeset includes
errors
and we do Repo.insert(changeset, conn: conn)
, it will not insert the data and instead will return {:error, _}
.