Ecto v1.1.0 Ecto.Changeset

Changesets allow filtering, casting, validation and definition of constraints when manipulating models.

There is an example of working with changesets in the introductory documentation in the Ecto module. The functions change/2 and cast/4 are the usual entry points for creating changesets, while the remaining functions are useful for manipulating them.

Validations and constraints

Ecto changesets provide both validations and constraints which are ultimately turned into errors in case something goes wrong.

The difference between them is that validations can be executed without a need to interact with the database and, therefore, are always executed before attemping to insert or update the entry in the database.

However, constraints can only be checked in a safe way when performing the operation in the database. As a consequence, validations are always checked before constraints. Constraints won’t even be checked in case validations failed.

Let’s see an example:

defmodule User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name
    field :email
    field :age, :integer
  end

  def changeset(user, params \\ :empty) do
    user
    |> cast(params, ~w(name email), ~w(age))
    |> validate_format(:email, ~r/@/)
    |> validate_inclusion(:age, 18..100)
    |> unique_constraint(:email)
  end
end

In the changeset/2 function above, we define two validations - one for checking the e-mail format and another to check the age - as well as a unique constraint in the email field.

Let’s suppose the e-mail is given but the age is invalid. The changeset would have the following errors:

changeset = User.changeset(%User{}, %{age: 0, email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [age: "is invalid"]

In this case, we haven’t checked the unique constraint in the e-mail field because the data did not validate. Let’s fix the age and assume, however, that the e-mail already exists in the database:

changeset = User.changeset(%User{}, %{age: 42, email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.errors #=> [email: "has already been taken"]

Validations and constraints define an explicit boundary when the check happens. By moving constraints to the database, we also provide a safe, correct and data-race free means of checking the user input.

The Ecto.Changeset struct

The fields are:

  • valid? - Stores if the changeset is valid
  • model - The changeset root model
  • params - The parameters as given on changeset creation
  • changes - The changes from parameters that were approved in casting
  • errors - All errors from validations
  • validations - All validations performed in the changeset
  • constraints - All constraints defined in the changeset
  • required - All required fields as a list of atoms
  • optional - All optional fields as a list of atoms
  • filters - Filters (as a map %{field => value}) to narrow the scope of update/delete queries
  • action - The action to be performed with the changeset
  • types - Cache of the model’s field types
  • repo - The repository applying the changeset (only set after a Repo function is called)
  • opts - The options given to the repository

On replace

Using changesets you can work with has_one and has_many associations as well as with embedded structs. When working with those relations, the :on_replace option can be given to cast_assoc/cast_embed as well as put_assoc/put_embed to configure the action that should be taken when the child model is no longer associated to the parent one.

This may be invoked in different occasions, for example, when it has been ommited in the list of associations/embeds for a many relation, or new struct was specified for a one relation.

Possible values are:

  • :raise (default) - do not allow removing association or embedded model via parent changesets,
  • :mark_as_invalid - if attempting to remove the association or embedded model via parent changeset - an error will be added to the parent changeset, and it will be marked as invalid,
  • :nilify - sets owner reference column to nil (available only for associations),
  • :delete - removes the association or related model from the database. This option has to be used carefully. You should consider adding a separate boolean virtual field to your model that will alow to manually mark it deletion, as in the example below:

    defmodule Comment do
      use Ecto.Schema
      import Ecto.Changeset
    
      schema "comments" do
        field :body, :string
        field :delete, :boolean, virtual: true
      end
    
      def changeset(model, params) do
        cast(model, params, [:body], [:delete])
        |> maybe_mark_for_deletion
      end
    
      defp maybe_mark_for_deletion(changeset) do
        if get_change(changeset, :delete) do
          %{changeset | action: :delete}
        else
          changeset
        end
      end
    end

on_replace may also be set when defining the association/embed.

Summary

Functions

Adds an error to the changeset

Applies the changeset changes to the changeset model

Checks the associated model exists

Converts the given params into a changeset for model keeping only the set of required and optional keys

Casts the given association

Wraps the given model in a changeset or adds changes to a changeset

Deletes a change with the given key

Checks for a exclude constraint in the given field

Fetches a change from the given changeset

Fetches the given field from changes or from the model

Forces a change on the given key with value

Checks for foreign key constraint in the given field

Gets a change or returns a default value

Gets a field from changes or from the model

Merges two changesets

Checks the associated model does not exist

Applies optimistic locking to the changeset

Provides a function to run before emitting changes to the repository

Puts the given association as change in the changeset

Puts a change on the given key with value

Puts the given embed as change in the changeset

Traverses changeset errors and applies function to error messages

Checks for a unique constraint in the given field

Validates the given field change

Stores the validation metadata and validates the given field change

Validates that the given field matches the confirmation parameter of that field

Validates a change is not included in the given enumerable

Validates a change has the given format

Validates a change is included in the given enumerable

Validates a change is a string or list of the given length

Validates the properties of a number

Validates a change, of type enum, is a subset of the given enumerable. Like validate_inclusion/4 for lists

Types

action :: nil | :insert | :update | :delete
cast_field ::
  String.t |
  atom |
  {atom, Ecto.Changeset.Relation.on_cast}
constraint :: %{type: :unique, constraint: String.t, field: atom, message: error_message}
error :: {atom, error_message}
t :: %Ecto.Changeset{action: action, changes: %{atom => term}, constraints: [constraint], errors: [error], filters: %{atom => term}, model: Ecto.Schema.t | nil, optional: [atom], opts: Keyword.t, params: %{String.t => term} | nil, prepare: [(t -> t)], repo: atom | nil, required: [atom], types: nil | %{atom => Ecto.Type.t}, valid?: boolean, validations: Keyword.t}

Functions

add_error(changeset, key, message)

Specs

add_error(t, atom, error_message) :: t

Adds an error to the changeset.

Examples

iex> changeset = change(%Post{}, %{title: ""})
iex> changeset = add_error(changeset, :title, "empty")
iex> changeset.errors
[title: "empty"]
iex> changeset.valid?
false
apply_changes(changeset)

Specs

apply_changes(t) :: Ecto.Schema.t

Applies the changeset changes to the changeset model.

Note this operation is automatically performed on Ecto.Repo.insert!/2 and Ecto.Repo.update!/2, however this function is provided for debugging and testing purposes.

Examples

apply_changes(changeset)
assoc_constraint(changeset, assoc, opts \\ [])

Specs

assoc_constraint(t, atom, Keyword.t) ::
  t |
  no_return

Checks the associated model exists.

This is similar to foreign_key_constraint/3 except that the field is inflected from the association definition. This is useful to guarantee that a child will only be created if the parent exists in the database too. Therefore, it only applies to belongs_to associations.

As the name says, a constraint is required in the database for this function to work. Such constraint is often added as a reference to the child table:

create table(:comments) do
    add :post_id, references(:posts)
  end

Now, when inserting a comment, it is possible to forbid any comment to be added if the associated post does not exist:

comment
  |> Ecto.Changeset.cast(params, ~w(post_id))
  |> Ecto.Changeset.assoc_constraint(:post)
  |> Repo.insert

Options

  • :message - the message in case the constraint check fails, defaults to “does not exist”
  • :name - the constraint name. By default, the constraint name is inflected from the table + association field. May be required explicitly for complex cases
cast(model_or_changeset, params, required, optional)

Specs

cast(Ecto.Schema.t | t, %{binary => term} | %{atom => term} | nil, [cast_field], [cast_field]) ::
  t |
  no_return

Converts the given params into a changeset for model keeping only the set of required and optional keys.

This function receives a model and some params, and casts the params according to the schema information from model. params is a map with string keys or a map with atom keys containing potentially unsafe data.

During casting, all valid parameters will have their key name converted to an atom and stored as a change in the :changes field of the changeset. All parameters that are not listed in required or optional are ignored.

If casting of all fields is successful and all required fields are present either in the model or in the given params, the changeset is returned as valid.

Examples

iex> changeset = cast(post, params, ~w(title), ~w())
iex> if changeset.valid? do
...>   Repo.update!(changeset)
...> end

Passing a changeset as the first argument:

iex> changeset = cast(post, %{title: "Hello"}, ~w(), ~w(title))
iex> new_changeset = cast(changeset, %{title: "Foo", body: "Bar"}, ~w(title), ~w(body))
iex> new_changeset.params
%{title: "Foo", body: "Bar"}
iex> new_changeset.required
[:title]
iex> new_changeset.optional
[:body]

Empty parameters

The params argument can also be the atom :empty. In such cases, the changeset is automatically marked as invalid, with an empty :changes map. This is useful to run the changeset through all validation steps for introspection:

iex> changeset = cast(post, :empty, ~w(title), ~w())
iex> changeset = validate_length(post, :title, min: 3)
iex> changeset.validations
[title: [min: 3]]

Composing casts

cast/4 also accepts a changeset instead of a model as its first argument. In such cases, all the effects caused by the call to cast/4 (additional and optional fields, errors and changes) are simply added to the ones already present in the argument changeset. Parameters are merged (not deep-merged) and the ones passed to cast/4 take precedence over the ones already in the changeset.

Note that if a field is marked both as required as well as optional (for example by being in the :required field of the argument changeset and also in the optional list passed to cast/4), then it will be marked as required and not optional. This represents the fact that required fields are “stronger” than optional fields.

Relations

You can override the relation’s on_cast setting by providing a 2 item tuple in the required or optional list instead of a simple field name.

The key will be the relation’s name and value is either the changeset function’s name or an anonymous function that accepts a model and params. The new function will be used similarily to the one provided in the on_cast setting.

# Will use Author.custom_changeset/2 as the changeset function
cast(post, %{author: %{name: "Paul"}}, ~w(), [{:author, :custom_changeset})

# Will use my_custom_changeset/2 as the changeset function.
cast(post, %{author: %{name: "Paul"}}, ~w(), [{:author, &my_custom_changeset/2}])

defp my_custom_changeset(model, params) do
  cast(model, params, ~w(name))
end
cast_assoc(changeset, name, opts \\ [])

Casts the given association.

The parameters for the given association will be retrieved from changeset.params and the changeset function in the association module will be invoked. The function to be invoked may also be configured by using the :with option.

The changeset must have been previously cast using cast/4 before this function is invoked.

Options

  • :with - the function to build the changeset from params. Defaults to the changeset/2 function in the association module
  • :on_replace - see “On Replace” section on Ecto.Changeset
cast_embed(changeset, name, opts \\ [])

Casts the given embed.

The parameters for the given embed will be retrieved from changeset.params and the changeset function in the embed module will be invoked. The function to be invoked may also be configured by using the :with option.

The changeset must have been previously cast using cast/4 before this function is invoked.

Options

  • :with - the function to build the changeset from params. Defaults to the changeset/2 function in the embed module
  • :on_replace - see “On Replace” section on Ecto.Changeset
change(model_or_changeset, changes \\ %{})

Specs

change(Ecto.Schema.t | t, %{atom => term} | Keyword.t) ::
  t |
  no_return

Wraps the given model in a changeset or adds changes to a changeset.

Changed attributes will only be added if the change does not have the same value as the attribute in the model.

This function is useful for:

  • wrapping a model inside a changeset
  • directly changing the model without performing castings nor validations
  • directly bulk-adding changes to a changeset

Since no validation nor casting is performed, change/2 expects the keys in changes to be atoms. changes can be a map as well as a keyword list.

When a changeset is passed as the first argument, the changes passed as the second argument are merged over the changes already in the changeset if they differ from the values in the model. If changes is an empty map, this function is a no-op.

See cast/4 if you’d prefer to cast and validate external parameters.

Examples

iex> changeset = change(%Post{})
%Ecto.Changeset{...}
iex> changeset.valid?
true
iex> changeset.changes
%{}

iex> changeset = change(%Post{author: "bar"}, title: "title")
iex> changeset.changes
%{title: "title"}

iex> changeset = change(%Post{title: "title"}, title: "title")
iex> changeset.changes
%{}

iex> changeset = change(changeset, %{title: "new title", body: "body"})
iex> changeset.changes.title
"new title"
iex> changeset.changes.body
"body"
delete_change(changeset, key)

Specs

delete_change(t, atom) :: t

Deletes a change with the given key.

Examples

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = delete_change(changeset, :title)
iex> get_change(changeset, :title)
nil
exclude_constraint(changeset, field, opts \\ [])

Checks for a exclude constraint in the given field.

The exclude constraint works by relying on the database to check if the exclude constraint has been violated or not and, if so, Ecto converts it into a changeset error.

Options

  • :message - the message in case the constraint check fails, defaults to “violates an exclusion constraint”
  • :name - the constraint name. By default, the constraint name is inflected from the table + field. May be required explicitly for complex cases
fetch_change(changeset, key)

Specs

fetch_change(t, atom) :: {:ok, term} | :error

Fetches a change from the given changeset.

This function only looks at the :changes field of the given changeset and returns {:ok, value} if the change is present or :error if it’s not.

Examples

iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
iex> fetch_change(changeset, :title)
{:ok, "bar"}
iex> fetch_change(changeset, :body)
:error
fetch_field(changeset, key)

Specs

fetch_field(t, atom) ::
  {:changes, term} |
  {:model, term} |
  :error

Fetches the given field from changes or from the model.

While fetch_change/2 only looks at the current changes to retrieve a value, this function looks at the changes and then falls back on the model, finally returning :error if no value is available.

For relations this functions will return the models with changes applied, as if they were taken from model. To retrieve raw changesets, please use fetch_change/2.

Examples

iex> post = %Post{title: "Foo", body: "Bar baz bong"}
iex> changeset = change(post, %{title: "New title"})
iex> fetch_field(changeset, :title)
{:changes, "New title"}
iex> fetch_field(changeset, :body)
{:model, "Bar baz bong"}
iex> fetch_field(changeset, :not_a_field)
:error
force_change(changeset, key, value)

Specs

force_change(t, atom, term) :: t | no_return

Forces a change on the given key with value.

If the change is already present, it is overridden with the new value.

Examples

iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
iex> changeset = force_change(changeset, :title, "bar")
iex> changeset.changes
%{title: "bar"}

iex> changeset = force_change(changeset, :author, "bar")
iex> changeset.changes
%{title: "bar", author: "bar"}
foreign_key_constraint(changeset, field, opts \\ [])

Specs

foreign_key_constraint(t, atom, Keyword.t) :: t

Checks for foreign key constraint in the given field.

The foreign key constraint works by relying on the database to check if the associated model exists or not. This is useful to guarantee that a child will only be created if the parent exists in the database too.

In order to use the foreign key constraint the first step is to define the foreign key in a migration. This is often done with references. For example, imagine you are creating a comments table that belongs to posts. One would have:

create table(:comments) do
  add :post_id, references(:posts)
end

By default, Ecto will generate a foreign key constraint with name “comments_post_id_fkey” (the name is configurable).

Now that a constraint exists, when creating comments, we could annotate the changeset with foreign key constraint so Ecto knows how to convert it into an error message:

cast(comment, params, ~w(post_id), ~w())
|> foreign_key_constraint(:post_id)

Now, when invoking Repo.insert/2 or Repo.update/2, if the associated post does not exist, it will be converted into an error and {:error, changeset} returned by the repository.

Options

  • :message - the message in case the constraint check fails, defaults to “does not exist”
  • :name - the constraint name. By default, the constraint name is inflected from the table + field. May be required explicitly for complex cases
get_change(changeset, key, default \\ nil)

Specs

get_change(t, atom, term) :: term

Gets a change or returns a default value.

Examples

iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
iex> get_change(changeset, :title)
"bar"
iex> get_change(changeset, :body)
nil
get_field(changeset, key, default \\ nil)

Specs

get_field(t, atom, term) :: term

Gets a field from changes or from the model.

While get_change/3 only looks at the current changes to retrieve a value, this function looks at the changes and then falls back on the model, finally returning default if no value is available.

For relations this functions will return the models with changes applied, as if they were taken from model. To retrieve raw changesets, please use get_change/3.

iex> post = %Post{title: "A title", body: "My body is a cage"}
iex> changeset = change(post, %{title: "A new title"})
iex> get_field(changeset, :title)
"A new title"
iex> get_field(changeset, :not_a_field, "Told you, not a field!")
"Told you, not a field!"
merge(changeset1, changeset2)

Specs

merge(t, t) :: t | no_return

Merges two changesets.

This function merges two changesets provided they have been applied to the same model (their :model field is equal); if the models differ, an ArgumentError exception is raised. If one of the changesets has a :repo field which is not nil, then the value of that field is used as the :repo field of the resulting changeset; if both changesets have a non-nil and different :repo field, an ArgumentError exception is raised.

The other fields are merged with the following criteria:

  • params - params are merged (not deep-merged) giving precedence to the params of changeset2 in case of a conflict. If both changesets have their :params fields set to nil, the resulting changeset will have its params set to nil too.
  • changes - changes are merged giving precedence to the changeset2 changes.
  • errors and validations - they are simply concatenated.
  • required and optional - they are merged; all the fields that appear in the optional list of either changesets and also in the required list of the other changeset are moved to the required list of the resulting changeset.

Examples

iex> changeset1 = cast(%{title: "Title"}, %Post{}, ~w(title), ~w(body))
iex> changeset2 = cast(%{title: "New title", body: "Body"}, %Post{}, ~w(title body), ~w())
iex> changeset = merge(changeset1, changeset2)
iex> changeset.changes
%{body: "Body", title: "New title"}
iex> changeset.required
[:title, :body]
iex> changeset.optional
[]

iex> changeset1 = cast(%{title: "Title"}, %Post{body: "Body"}, ~w(title), ~w(body))
iex> changeset2 = cast(%{title: "New title"}, %Post{}, ~w(title), ~w())
iex> merge(changeset1, changeset2)
** (ArgumentError) different models when merging changesets
no_assoc_constraint(changeset, assoc, opts \\ [])

Specs

no_assoc_constraint(t, atom, Keyword.t) ::
  t |
  no_return

Checks the associated model does not exist.

This is similar to foreign_key_constraint/3 except that the field is inflected from the association definition. This is useful to guarantee that parent can only be deleted (or have its primary key changed) if no child exists in the database. Therefore, it only applies to has_* associations.

As the name says, a constraint is required in the database for this function to work. Such constraint is often added as a reference to the child table:

create table(:comments) do
    add :post_id, references(:posts)
  end

Now, when deleting the post, it is possible to forbid any post to be deleted if they still have comments attached to it:

post
  |> Ecto.Changeset.change
  |> Ecto.Changeset.no_assoc_constraint(:comments)
  |> Repo.delete

Options

  • :message - the message in case the constraint check fails, defaults to “is still associated to this entry” (for has_one) and “are still associated to this entry” (for has_many)
  • :name - the constraint name. By default, the constraint name is inflected from the association table + association field. May be required explicitly for complex cases
optimistic_lock(model_or_changeset, field, incrementer \\ &(&1 + 1))

Specs

optimistic_lock(Ecto.Schema.t | t, atom, (integer -> integer)) ::
  t |
  no_return

Applies optimistic locking to the changeset.

Optimistic locking (or optimistic concurrency control) is a technique that allows concurrent edits on a single record. While pessimistic locking works by locking a resource for an entire transaction, optimistic locking only checks if the resource changed before updating it.

This is done by regularly fetching the record from the database, then checking whether another user has made changes to the record only when updating the record. This behaviour is ideal in situations where the chances of concurrent updates to the same record are low; if they’re not, pessimistic locking or other concurrency patterns may be more suited.

Usage

Optimistic locking works by keeping a “version” counter for each record; this counter gets incremented each time a modification is made to a record. Hence, in order to use optimistic locking, a field must exist in your schema for versioning purpose. Such field is usually an integer but other types are supported.

Examples

Assuming we have a Post schema (stored in the posts table), the first step is to add a version column to the posts table:

alter table(:posts) do
  add :lock_version, :integer, default: 1
end

The column name is arbitrary and doesn’t need to be :lock_version. Now add a field to the schema too:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    field :title, :string
    field :lock_version, :integer, default: 1
  end

  def changeset(:update, struct, params \\ :empty) do
    struct
    |> Ecto.Changeset.cast(struct, params, ~w(:title))
    |> Ecto.Changeset.optimistic_lock(:lock_version)
  end
end

Now let’s take optimistic locking for a spin:

iex> post = Repo.insert!(%Post{title: "foo"})
%Post{id: 1, title: "foo", lock_version: 1}
iex> valid_change = Post.changeset(:update, post, %{title: "bar"})
iex> stable_change = Post.changeset(:update, post, %{title: "baz"})
iex> Repo.update!(valid_change)
%Post{id: 1, title: "bar", lock_version: 2}
iex> Repo.update!(stale_change)
** (Ecto.StaleModelError) attempted to update a stale model:

%Post{id: 1, title: "baz", lock_version: 1}

When a conflict happens (a record which has been previously fetched is being updated, but that same record has been modified since it was fetched), an Ecto.StaleModelError exception is raised.

Optimistic locking also works with delete operations. Just call the optimistic_lock function with the model before delete:

iex> changeset = Ecto.Changeset.optimistic_lock(post, :lock_version)
iex> Repo.delete(changeset)

Finally, keep in optimistic_lock/3 by default assumes the field being used as a lock is an integer. If you want to use another type, you need to pass the third argument customizing how the next value is generated:

iex> Ecto.Changeset.optimistic_lock(post, :lock_uuid, fn _ -> Ecto.UUID.generate end)
prepare_changes(changeset, function)

Specs

prepare_changes(t, (t -> t)) :: t

Provides a function to run before emitting changes to the repository.

Such function receives the changeset and must return a changeset, allowing developers to do final adjustments to the changeset or to issue data consistency commands.

The given function is guaranteed to run inside the same transaction as the changeset operation for databases that do support transactions.

put_assoc(changeset, name, value, opts \\ [])

Puts the given association as change in the changeset.

The association may either be the association struct or a changeset for the given association.

If the association has no changes, it will be skipped. If the association is invalid, the changeset will be marked as invalid. If the given value is not an association, it will raise.

Options

put_change(changeset, key, value)

Specs

put_change(t, atom, term) :: t | no_return

Puts a change on the given key with value.

If the change is already present, it is overridden with the new value, also, if the change has the same value as the model, it is not added to the list of changes.

Examples

iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
iex> changeset = put_change(changeset, :title, "bar")
iex> changeset.changes
%{title: "bar"}

iex> changeset = put_change(changeset, :author, "bar")
iex> changeset.changes
%{title: "bar"}
put_embed(changeset, name, value, opts \\ [])

Puts the given embed as change in the changeset.

The embed may either be the embed struct or a changeset for the given embed.

If the embed has no changes, it will be skipped. If the embed is invalid, the changeset will be marked as invalid. If the given value is not an embed, it will raise.

Options

traverse_errors(changeset, msg_func)

Specs

traverse_errors(t, (error_message -> String.t)) :: %{atom => String.t}

Traverses changeset errors and applies function to error messages.

This function is particularly useful when associations and embeds are cast in the changeset as it will traverse all associations and embeds and place all errors in a series of nested maps.

A changeset is supplied along with a function to apply to each error message as the changeset is traversed. The error message function receives a single argument matching either:

  • {message, opts} - The string error message and options, for example {"should be at least %{count} characters", [count: 3]}
  • message - The string error message

Examples

iex> traverse_errors(changeset, fn
  {msg, opts} -> String.replace(msg, "%{count}", to_string(opts[:count]))
  msg -> msg
end)
%{title: "should be at least 3 characters"}
unique_constraint(changeset, field, opts \\ [])

Specs

unique_constraint(t, atom, Keyword.t) :: t

Checks for a unique constraint in the given field.

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 unique constraint so Ecto knows how to convert it into an error message:

cast(user, params, ~w(email), ~w())
|> 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 inflected from the table + field. May be required explicitly for complex cases

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. We would write in a migration:

create unique_index(:users, [:email, :company_id])

Because such indexes have usually more complex names, we need to explicitly tell the changeset which constraint name to use:

cast(user, params, ~w(email), ~w())
|> unique_constraint(:email, name: :posts_email_company_id_index)

Alternatively, you can give both unique_index and unique_constraint a name:

# In the migration
create unique_index(:users, [:email, :company_id], name: :posts_special_email_index)

# In the model
cast(user, params, ~w(email), ~w())
|> unique_constraint(:email, name: :posts_email_company_id_index)

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.

If for some reason your database does not support case insensitive columns, you can explicitly downcase values before inserting/updating them:

cast(model, params, ~w(email), ~w())
|> update_change(:email, &String.downcase/1)
|> unique_constraint(:email)
update_change(changeset, key, function)

Specs

update_change(t, atom, (term -> term)) :: t

Updates a change.

The given function is invoked with the change value only if there is a change for the given key. Note that the value of the change can still be nil (unless the field was marked as required on cast/4).

Examples

iex> changeset = change(%Post{}, %{impressions: 1})
iex> changeset = update_change(changeset, :impressions, &(&1 + 1))
iex> changeset.changes.impressions
2
validate_change(changeset, field, validator)

Specs

validate_change(t, atom, (atom, term -> [error])) :: t

Validates the given field change.

It invokes the validator function to perform the validation only if a change for the given field exists and the change value is not nil. The function must return a list of errors (with an empty list meaning no errors).

In case there’s at least one error, the list of errors will be appended to the :errors field of the changeset and the :valid? flag will be set to false.

Examples

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = validate_change changeset, :title, fn
...>   # Value must not be "foo"!
...>   :title, "foo" -> [title: "is foo"]
...>   :title, _     -> []
...> end
iex> changeset.errors
[title: "is_foo"]
validate_change(changeset, field, metadata, validator)

Specs

validate_change(t, atom, term, (atom, term -> [error])) :: t

Stores the validation metadata and validates the given field change.

Similar to validate_change/3 but stores the validation metadata into the changeset validators. The validator metadata is often used as a reflection mechanism, to automatically generate code based on the available validations.

Examples

iex> changeset = change(%Post{}, %{title: "foo"})
iex> changeset = validate_change changeset, :title, :useless_validator, fn
...>   _, _ -> []
...> end
iex> changeset.validations
[title: :useless_validator]
validate_confirmation(changeset, field, opts \\ [])

Specs

validate_confirmation(t, atom, Keyword.t) :: t

Validates that the given field matches the confirmation parameter of that field.

By calling validate_confirmation(changeset, :email), this validation will check if both “email” and “email_confirmation” in the parameter map matches.

Note that this does not add a validation error if the confirmation field is nil. Note “email_confirmation” does not need to be added as a virtual field in your schema.

Options

  • :message - the message on failure, defaults to “does not match”

Examples

validate_confirmation(changeset, :email)
validate_confirmation(changeset, :password, message: "does not match password")

cast(model, params, ~w(password), ~w())
|> validate_confirmation(:password, message: "does not match password")
validate_exclusion(changeset, field, data, opts \\ [])

Specs

validate_exclusion(t, atom, Enum.t, Keyword.t) :: t

Validates a change is not included in the given enumerable.

Options

  • :message - the message on failure, defaults to “is reserved”

Examples

validate_exclusion(changeset, :name, ~w(admin superadmin))
validate_format(changeset, field, format, opts \\ [])

Specs

validate_format(t, atom, Regex.t, Keyword.t) :: t

Validates a change has the given format.

The format has to be expressed as a regular expression.

Options

  • :message - the message on failure, defaults to “has invalid format”

Examples

validate_format(changeset, :email, ~r/@/)
validate_inclusion(changeset, field, data, opts \\ [])

Specs

validate_inclusion(t, atom, Enum.t, Keyword.t) :: t

Validates a change is included in the given enumerable.

Options

  • :message - the message on failure, defaults to “is invalid”

Examples

validate_inclusion(changeset, :gender, ["man", "woman", "other", "prefer not to say"])
validate_inclusion(changeset, :age, 0..99)
validate_length(changeset, field, opts)

Specs

validate_length(t, atom, Keyword.t) :: t

Validates a change is a string or list of the given length.

Options

  • :is - the length must be exactly this value
  • :min - the length must be greater than or equal to this value
  • :max - the length must be less than or equal to this value
  • :message - the message on failure, depending on the validation, is one of:

    • for strings:
    • “should be %{count} character(s)”
    • “should be at least %{count} character(s)”
    • “should be at most %{count} character(s)”
    • for lists:
    • “should have %{count} item(s)”
    • “should have at least %{count} item(s)”
    • “should have at most %{count} item(s)”

Examples

validate_length(changeset, :title, min: 3)
validate_length(changeset, :title, max: 100)
validate_length(changeset, :title, min: 3, max: 100)
validate_length(changeset, :code, is: 9)
validate_length(changeset, :topics, is: 2)
validate_number(changeset, field, opts)

Specs

validate_number(t, atom, Keyword.t) ::
  t |
  no_return

Validates the properties of a number.

Options

  • :less_than
  • :greater_than
  • :less_than_or_equal_to
  • :greater_than_or_equal_to
  • :equal_to
  • :message - the message on failure, defaults to one of:

    • “must be less than %{count}”
    • “must be greater than %{count}”
    • “must be less than or equal to %{count}”
    • “must be greater than or equal to %{count}”
    • “must be equal to %{count}”

Examples

validate_number(changeset, :count, less_than: 3)
validate_number(changeset, :pi, greater_than: 3, less_than: 4)
validate_number(changeset, :the_answer_to_life_the_universe_and_everything, equal_to: 42)
validate_subset(changeset, field, data, opts \\ [])

Specs

validate_subset(t, atom, Enum.t, Keyword.t) :: t

Validates a change, of type enum, is a subset of the given enumerable. Like validate_inclusion/4 for lists.

Options

  • :message - the message on failure, defaults to “has an invalid entry”

Examples

validate_subset(changeset, :pets, ["cat", "dog", "parrot"])
validate_subset(changeset, :lottery_numbers, 0..99)