Ecto changesets, validations and constraints

Now that we've had a look at using change and cast to create changesets from IEx, let's create changeset functions in our schemas and add validations to them.

Validations

Ecto provides a number of functions for creating validations. Each takes a changeset as its first parameter and returns a changeset that will have newly-added errors if the validation has failed. Three of the most common are:

  • validate_required(cset, list_of_fields): adds an error to the changeset if it doesn't include changes for each of the fields.
  • validate_length(cset, field, options): adds an error to the changeset if the atom passed into field doesn't match the length specified by options. Common options are is: <integer>, min: <integer> and max: <integer>. E.g. validate_length(cset, :password, min: 8).
  • validate_format(cset, field, regex): adds an error to the changeset if the atom passed into field doesn't match the regular expression passed into regex.

Custom validations

We can also create custom validations that validate a changeset based on any arbitrary function. For these, use:

  • validate_change(cset, name, function): adds whatever errors to the changeset that function returns, with the label of the atom passed into name.

To illustrate this, we'll create an arbitrary validation called "older_than_13". The function will take a field of :birth_date, which will be passed in from the changeset. We'll add a birth_date field to the Users schema so that it will be present in the changeset. Since there is no "birth_date" field in the database schema for "users" we'll specify it as a virtual field in the ecto schema.

field(:birth_date, :date, virtual: true)

Then the "older_than_13" function will get the current date and check if the birth date is 13 years earlier. It returns an empty list if everything is okay. If the user is too young, it returns an error of the following form:

[birth_date: "Must be over 13 years old!"]

def older_than_13(:birth_date, %Date{} = birth_date) do
  {year, month, date} = Date.to_erl(Date.utc_today())
  min_date = Date.from_erl!({year - 13, month, date})
  case Date.compare(min_date, birth_date) do
    :lt -> [birth_date: "Must be over 13 years old!"]
    _ -> []
  end
end

Note that the above function is simple but it will fail on February 29th on leap years! Use a library to handle fraught calendar-related tasks.

Putting it all together

After adding all of the validations above into a changeset, this is the state of our User module:

defmodule Linkly.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias Linkly.{Bookmark, Link, LinkTag, Tag, User}

  schema "users" do
    field(:about)
    field(:birth_date, :date, virtual: true)
    field(:email)
    field(:username)
    has_many(:bookmarks, Bookmark)
    has_many(:bookmarked_links, through: [:bookmarks, :link])
    has_many(:taggings, LinkTag)
    many_to_many(:tagged_links, Link, join_through: LinkTag)
    many_to_many(:tags, Tag, join_through: LinkTag)


    timestamps()
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:username, :about, :email, :birth_date])
    |> validate_required([:username, :email, :birth_date])
    |> validate_length(:username, min: 3)
    |> validate_format(:email, ~r/@/)
    |> validate_change(:birth_date, &older_than_13/2)
    |> unique_constraint(:email)
    |> unique_constraint(:username)
  end

  def older_than_13(:birth_date, %Date{} = birth_date) do
    {year, month, date} = Date.to_erl(Date.utc_today())
    min_date = Date.from_erl!({year - 13, month, date})
    case Date.compare(min_date, birth_date) do
      :lt -> [birth_date: "Must be over 13 years old!"]
      _ -> []
    end
  end
end
Back to index

No Comments