Ecto v2.2.10 Ecto.Changeset View Source

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

There is an example of working with changesets in the introductory documentation in the Ecto module. The functions cast/4 and change/2 are the usual entry points for creating changesets. The first one is used to cast and validate external parameters, such as parameters sent through a form, API, command line, etc. The second one is used to change data directly from your application.

The remaining functions in this module, such as validations, constraints, association handling, are about manipulating changesets. Let’s discuss some of this extra functionality.

External vs internal data

Changesets allow working with both kinds of data:

  • internal to the application - for example programatically generated, or coming from other subsystems. This use case is primarily covered by the change/2 and put_change/3 functions.

  • external to the application - for example data provided by the user in a form that needs to be type-converted and properly validated. This use case is primarily covered by the cast/4 function.

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 most validations can be executed without a need to interact with the database and, therefore, are always executed before attempting to insert or update the entry in the database. Some validations may happen against the database but they are inherently unsafe. Those validations start with a unsafe_ prefix, such as unsafe_validate_unique/3.

On the other hand, constraints rely on the database and are always safe. 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 \\ %{}) do
    user
    |> cast(params, [:name, :email, :age])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_inclusion(:age, 18..100)
    |> unique_constraint(:email)
  end
end

In the changeset/2 function above, we define three validations. They check that name and email fields are present in the changeset, the e-mail is of the specified format, and the age is between 18 and 100 - 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", []}, name: {"can't be blank", []}]

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.

Empty values

Many times, the data given on cast needs to be further pruned, specially regarding empty values. For example, if you are gathering data to be cast from the command line or through an HTML form or any other text-based format, it is likely those means cannot express nil values. For those reasons, changesets include the concept of empty values, which are values that will be automatically converted to the field’s default value on cast/4. Those values are stored in the changeset empty_values field and default to [""].

Associations, embeds and on replace

Using changesets you can work with associations as well as with embedded structs. Changesets provide a convenient way to working with associations as whole values - for example considering the entire list of has_many associations and not focusing just on a single one. Two main functions that provide this functionality are cast_assoc/3 for working with external data, and put_assoc/3 for working with internal data - the difference between those two functions is analogical to the difference between cast/4 and change/2.

Sometimes related data may be replaced by incoming data and by default Ecto won’t allow such. Such behaviour can be changed when defining the relation by setting :on_replace option in your association/embed definition according to the values below:

  • :raise (default) - do not allow removing association or embedded data via parent changesets
  • :mark_as_invalid - if attempting to remove the association or embedded data 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)
  • :update - updates the association, available only for has_one and belongs_to. This option will update all the fields given to the changeset including the id for the association
  • :delete - removes the association or related data from the database. This option has to be used carefully

The :delete option in particular must be used carefully as it would allow users to delete any associated data. If you need deletion, it is often preferred to add a separate boolean virtual field to the changeset function that will allow you to manually mark it for 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(comment, params) do
    cast(comment, 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

Schemaless changesets

In the changeset examples so far, we have always used changesets to validate and cast data contained in a struct, such as the %User{} struct defined by the User module.

However, changesets can also be used with data in a plain map, by passing a tuple containing both the data and the supported types:

data  = %{}
types = %{first_name: :string, last_name: :string, email: :string}

changeset =
  {data, types}
  |> Ecto.Changeset.cast(params["sign_up"], Map.keys(types))
  |> validate_required(...)
  |> validate_length(...)

Such functionality makes Ecto extremely useful to cast, validate and prune data even if it is not meant to be persisted to the database.

Changeset actions

Changesets have an action field which is usually set by Ecto.Repo whenever one of the operations such as insert or update is called:

changeset = User.changeset(%User{}, %{age: 42, email: "mary@example.com"})
{:error, changeset} = Repo.insert(changeset)
changeset.action
#=> :insert

This means that when working with changesets that are not meant to be persisted to the database, such as schemaless changesets, you may need to explicitly set the action to one specific value. Frameworks such as Phoenix uses the action value to define how a HTML forms should act.

Instead of setting the action manually, you may use apply_action/2 that emulates operations such as Repo.insert. apply_action/2 will return {:ok, changes} if the changeset is valid or {:error, changeset}, with the given action set in the changeset in case of errors.

The Ecto.Changeset struct

The fields are:

  • valid? - Stores if the changeset is valid
  • data - The changeset source data, for example, a struct
  • 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
  • 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 data’s field types
  • empty_values - A list of values to be considered empty
  • repo - The repository applying the changeset (only set after a Repo function is called)
  • repo_opts - A keyword list of options given to the underlying repository operation

Link to this section Summary

Functions

Adds an error to the changeset

Applies the changeset action only if the changes are valid

Applies the changeset changes to the changeset data

Checks the associated field exists

Applies the given params as changes for the given data according to the given set of keys. Returns a changeset

Casts the given association with the changeset parameters

Casts the given embed with the changeset parameters

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

Checks for a check constraint in the given field

Deletes a change with the given key

Checks for an exclusion constraint in the given field

Fetches a change from the given changeset

Fetches the given field from changes or from the data

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 data

Merges two changesets

Checks the associated field 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 a change in the changeset

Puts a change on the given key with value

Puts the given embed as a change in the changeset

Traverses changeset errors and applies the given function to error messages

Checks for a unique constraint in the given field

Validates that no existing record with a different primary key has the same values for these fields

Validates the given parameter was given as true

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 that one or more fields are present in the changeset

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

Link to this section Types

Link to this type action() View Source
action() :: nil | :insert | :update | :delete | :replace | :ignore
Link to this type constraint() View Source
constraint() :: %{
  type: :unique,
  constraint: String.t(),
  match: :exact | :suffix,
  field: atom(),
  message: error()
}
Link to this type t() View Source
t() :: %Ecto.Changeset{
  action: action(),
  changes: %{optional(atom()) => term()},
  constraints: [constraint()],
  data: Ecto.Schema.t() | map() | nil,
  empty_values: term(),
  errors: [{atom(), error()}],
  filters: %{optional(atom()) => term()},
  params: %{optional(String.t()) => term()} | nil,
  prepare: [(t() -> t())],
  repo: atom() | nil,
  repo_opts: Keyword.t(),
  required: [atom()],
  types: nil | %{optional(atom()) => Ecto.Type.t()},
  valid?: boolean(),
  validations: Keyword.t()
}

Link to this section Functions

Link to this function add_error(changeset, key, message, keys \\ []) View Source
add_error(t(), atom(), String.t(), Keyword.t()) :: t()

Adds an error to the changeset.

An additional keyword list keys can be passed to provide additional contextual information for the error. This is useful when using traverse_errors/2

Examples

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

iex> changeset = change(%Post{}, %{title: ""})
iex> changeset = add_error(changeset, :title, "empty", additional: "info")
iex> changeset.errors
[title: {"empty", [additional: "info"]}]
iex> changeset.valid?
false
Link to this function apply_action(changeset, action) View Source
apply_action(t(), action()) ::
  {:ok, Ecto.Schema.t() | data()} | {:error, t()}

Applies the changeset action only if the changes are valid.

If the changes are valid, all changes are applied to the changeset data. If the changes are invalid, no changes are applied, and an error tuple is returned with the changeset containing the action that was attempted to be applied.

The action may be one of :insert, :update, :delete, :replace.

Examples

iex> {:ok, data} = apply_action(changeset, :update)

iex> {:error, changeset} = apply_action(changeset, :update)
%Ecto.Changeset{action: :update}
Link to this function apply_changes(changeset) View Source
apply_changes(t()) :: Ecto.Schema.t() | data()

Applies the changeset changes to the changeset data.

This operation will return the underlying data with changes regardless if the changeset is valid or not.

Examples

iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
iex> apply_changes(changeset)
%Post{author: "bar", title: "foo"}
Link to this function assoc_constraint(changeset, assoc, opts \\ []) View Source
assoc_constraint(t(), atom(), Keyword.t()) :: t() | no_return()

Checks the associated field exists.

This is similar to foreign_key_constraint/3 except that the field is inferred 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, [: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 inferred from the table + association field. May be required explicitly for complex cases
Link to this function cast(data, params, permitted, opts \\ []) View Source
cast(
  Ecto.Schema.t() | t() | {data(), types()},
  %{optional(binary()) => term()} | %{optional(atom()) => term()} | :invalid,
  [String.t() | atom()],
  Keyword.t()
) :: t() | no_return()

Applies the given params as changes for the given data according to the given set of keys. Returns a changeset.

The given data may be either a changeset, a schema struct or a {data, types} tuple. The second argument is a map of params that are cast according to the type information from data. params is a map with string keys or a map with atom keys containing potentially unsafe data.

During casting, all permitted 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 explicitly permitted are ignored.

If casting of all fields is successful, the changeset is returned as valid.

Options

  • :empty_values - a list of values to be considered as empty when casting. Defaults to the changeset value, which defaults to [""]

Examples

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

Passing a changeset as the first argument:

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

Or creating a changeset from a simple map with types:

iex> data = %{title: "hello"}
iex> types = %{title: :string}
iex> changeset = cast({data, types}, %{title: "world"}, [:title])
iex> apply_changes(changeset)
%{title: "world"}

Composing casts

cast/4 also accepts a changeset as its first argument. In such cases, all the effects caused by the call to cast/4 (additional 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.

Link to this function cast_assoc(changeset, name, opts \\ []) View Source

Casts the given association with the changeset parameters.

This function should be used when working with the entire association at once (and not a single element of a many-style association) and using data external to the application.

When updating the data, this function requires the association to have been preloaded in the changeset struct. Missing data will invoke the :on_replace behaviour defined on the association. Preloading is not necessary for newly built structs.

The parameters for the given association will be retrieved from changeset.params. Those parameters are expected to be a map with attributes, similar to the ones passed to cast/4. Once parameters are retrieved, cast_assoc/3 will match those parameters with the associations already in the changeset record.

For example, imagine a user has many addresses relationship where post data is sent as follows

%{"name" => "john doe", "addresses" => [
  %{"street" => "somewhere", "country" => "brazil", "id" => 1},
  %{"street" => "elsewhere", "country" => "poland"},
]}

and then

user
|> Repo.preload(:addresses)
|> Ecto.Changeset.cast(params, [])
|> Ecto.Changeset.cast_assoc(:addresses)

Once cast_assoc/3 is called, Ecto will compare those parameters with the addresses already associated with the user and act as follows:

  • If the parameter does not contain an ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
  • If the parameter contains an ID and there is no associated child with such ID, the parameter data will be passed to changeset/2 with a new struct and become an insert operation
  • If the parameter contains an ID and there is an associated child with such ID, the parameter data will be passed to changeset/2 with the existing struct and become an update operation
  • If there is an associated child with an ID and its ID is not given as parameter, the :on_replace callback for that association will be invoked (see the “On replace” section on the module documentation)

Every time the changeset/2 function is invoked, it must return a changeset. Note developers are allowed to explicitly set the :action field of a changeset to instruct Ecto how to act in certain situations. Let’s suppose that, if one of the associations has only empty fields, you want to ignore the entry altogether instead of showing an error. The changeset function could be written like this:

def changeset(struct, params) do
  struct
  |> cast(struct, params, [:title, :body])
  |> validate_requited([:title, :body])
  |> case do
    %{valid?: false, changes: changes} = changeset when changes == %{} ->
      # If the changeset is invalid and has no changes, it is
      # because all required fields are missing, so we ignore it.
      %{changeset | action: :ignore}
    changeset ->
      changeset
  end
end

Alternatives to cast_assoc/3

cast_assoc/3 is useful when the associated data is managed alongside the parent struct, all at once.

To work with a single element of an association, other functions are more appropriate. For example to insert a single associated struct for a has_many association it’s much easier to construct the associated struct with Ecto.build_assoc/3 and persist it directly with Ecto.Repo.insert/2.

Furthermore, if each side of the association is managed separately, it is preferable to use put_assoc/3 and directly instruct Ecto how the association should look like.

For example, imagine you are receiving a set of tags you want to associate to an user. Those tags are meant to exist upfront. Using cast_assoc/3 won’t work as desired because the tags are not managed alongside the user. In such cases, put_assoc/3 will work as desired. With the given parameters:

%{"name" => "john doe", "tags" => ["learner"]}

and then:

tags = Repo.all(from t in Tag, where: t.name in ^params["tags"])

user
|> Repo.preload(:tags)
|> Ecto.Changeset.cast(params) # No need to allow :tags as we put them directly
|> Ecto.Changeset.put_assoc(:tags, tags) # Explicitly set the tags

Note 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
  • :required - if the association is a required field
  • :required_message - the message on failure, defaults to “can’t be blank”
  • :invalid_message - the message on failure, defaults to “is invalid”
Link to this function cast_embed(changeset, name, opts \\ []) View Source

Casts the given embed with the changeset parameters.

The parameters for the given embed will be retrieved from changeset.params. Those parameters are expected to be a map with attributes, similar to the ones passed to cast/4. Once parameters are retrieved, cast_embed/3 will match those parameters with the embeds already in the changeset record. See cast_assoc/3 for an example of working with casts and associations which would also apply for embeds.

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
  • :required - if the embed is a required field
  • :required_message - the message on failure, defaults to “can’t be blank”
  • :invalid_message - the message on failure, defaults to “is invalid”
Link to this function change(data, changes \\ %{}) View Source
change(
  Ecto.Schema.t() | t() | {data(), types()},
  %{optional(atom()) => term()} | Keyword.t()
) :: t() | no_return()

Wraps the given data 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 field in the data.

This function is useful for:

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

The function is meant for working with data internal to the application. Because of that neither validation nor casting is performed. This means change/2 expects the keys in the changes map or keyword to be atoms.

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 struct. If changes is an empty map, this function is a no-op.

When a {data, types} is passed as the first argument, a changeset is created with the given data and types and marked as valid.

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"
Link to this function check_constraint(changeset, field, opts \\ []) View Source

Checks for a check constraint in the given field.

The check constraint works by relying on the database to check if the check 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 “is invalid”
  • :name - the name of the constraint. Required.
  • :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 which ends_with? :name to this changeset constraint.
Link to this function delete_change(changeset, key) View Source
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
Link to this function exclusion_constraint(changeset, field, opts \\ []) View Source

Checks for an exclusion constraint in the given field.

The exclusion constraint works by relying on the database to check if the exclusion 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 inferred from the table + field. 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 which ends_with? :name to this changeset constraint.
Link to this function fetch_change(changeset, key) View Source
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
Link to this function fetch_field(changeset, key) View Source
fetch_field(t(), atom()) :: {:changes, term()} | {:data, term()} | :error

Fetches the given field from changes or from the data.

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 data, finally returning :error if no value is available.

For relations, these functions will return the changeset original data with changes applied. 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)
{:data, "Bar baz bong"}
iex> fetch_field(changeset, :not_a_field)
:error
Link to this function force_change(changeset, key, value) View Source
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"}
Link to this function foreign_key_constraint(changeset, field, opts \\ []) View Source
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 data 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, [:post_id])
|> 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 inferred from the table + field. May be required explicitly for complex cases
Link to this function get_change(changeset, key, default \\ nil) View Source
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
Link to this function get_field(changeset, key, default \\ nil) View Source
get_field(t(), atom(), term()) :: term()

Gets a field from changes or from the data.

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 data, finally returning default if no value is available.

For relations, these functions will return the changeset data with changes applied. 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!"
Link to this function merge(changeset1, changeset2) View Source
merge(t(), t()) :: t() | no_return()

Merges two changesets.

This function merges two changesets provided they have been applied to the same data (their :data field is equal); if the data differs, 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 - required fields are merged; all the fields that appear in the required list of both changesets are moved to the required list of the resulting changeset.

Examples

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

iex> changeset1 = cast(%Post{body: "Body"}, %{title: "Title"}, [:title])
iex> changeset2 = cast(%Post{}, %{title: "New title"}, [:title])
iex> merge(changeset1, changeset2)
** (ArgumentError) different :data when merging changesets
Link to this function no_assoc_constraint(changeset, assoc, opts \\ []) View Source
no_assoc_constraint(t(), atom(), Keyword.t()) :: t() | no_return()

Checks the associated field does not exist.

This is similar to foreign_key_constraint/3 except that the field is inferred 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 with this entry” (for has_one) and “are still associated with this entry” (for has_many)
  • :name - the constraint name. By default, the constraint name is inferred from the association table + association field. May be required explicitly for complex cases
Link to this function optimistic_lock(data_or_changeset, field, incrementer \\ &(&1 + 1)) View Source
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 \\ %{}) do
    struct
    |> Ecto.Changeset.cast(params, [: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> stale_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.StaleEntryError) attempted to update a stale entry:

%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.StaleEntryError exception is raised.

Optimistic locking also works with delete operations. Just call the optimistic_lock/3 function with the data before delete:

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

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)
Link to this function prepare_changes(changeset, function) View Source
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.

Example

A common use case is updating a counter cache, in this case updating a post’s comment count when a comment is created:

def create_comment(comment, params) do
  comment
  |> cast(params, [:body, :post_id])
  |> prepare_changes(fn changeset ->
    assoc(changeset.data, :post)
    |> changeset.repo.update_all(inc: [comment_count: 1])
    changeset
  end)
end

We retrieve the repo from the comment changeset itself and use update_all to update the counter cache in one query. Finally, the original changeset must be returned.

Link to this function put_assoc(changeset, name, value, opts \\ []) View Source

Puts the given association as a change in the changeset.

This function should be used when working with the entire association at once (and not a single element of a many-style association) and using data internal to the application.

When updating the data, this function requires the association to have been preloaded in the changeset struct. Missing data will invoke the :on_replace behaviour defined on the association. Preloading is not necessary for newly built structs.

The given value may either be the association struct, a changeset for the given association or a map or keyword list of changes to be applied to the current association. On all cases, it is expected the keys to be atoms. If a map or keyword list are given and there is no association, one will be created.

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 any of the above, it will raise.

Also see cast_assoc/3 for a discussion of when to use cast_assoc/3 and put_assoc/3.

Although it accepts an opts argument, there are no options currently supported by put_assoc/4.

Link to this function put_change(changeset, key, value) View Source
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 in the changeset data, it is not added to the list of changes.

The function is meant for working with data internal to the application.

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"}
Link to this function put_embed(changeset, name, value, opts \\ []) View Source

Puts the given embed as a change in the changeset.

The given value may either be the embed struct, a changeset for the given embed or a map or keyword list of changes to be applied to the current embed. On all cases, it is expected the keys to be atoms. If a map or keyword list are given and there is no embed, one will be created.

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 struct or changeset, it will raise.

Also see cast_assoc/3 for a discussion of when to use cast_assoc/3 and put_assoc/3 which also applies to put_embed/3.

Although it accepts an opts argument, there are no options currently supported by put_embed/4.

Link to this function traverse_errors(changeset, msg_func) View Source
traverse_errors(
  t(),
  (error() -> String.t()) | (Ecto.Changeset.t(), atom(), error() -> String.t())
) :: %{optional(atom()) => [String.t()]}

Traverses changeset errors and applies the given 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 an error tuple {msg, opts}, for example:

{"should be at least %{count} characters", [count: 3, validation: :length, min: 3]}

Examples

iex> traverse_errors(changeset, fn {msg, opts} ->
...>   Enum.reduce(opts, msg, fn {key, value}, acc ->
...>     String.replace(acc, "%{#{key}}", to_string(value))
...>   end)
...> end)
%{title: ["should be at least 3 characters"]}

Optionally function can accept three arguments: changeset, field and error tuple {msg, opts}. It is useful whenever you want to extract validations rules from changeset.validations to build detailed error description.

Link to this function unique_constraint(changeset, field, opts \\ []) View Source
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, [: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. 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 which ends_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. 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 (here we’re using the naming convention that unique_index uses):

cast(user, params, [:email])
|> unique_constraint(:email, name: :users_email_company_id_index)

Notice that the first param is just one of the unique index fields, this 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", []}]

Alternatively, you can give both unique_index and unique_constraint the same name:

# 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)

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)
Link to this function unsafe_validate_unique(changeset, fields, repo, opts \\ []) View Source

Validates that no existing record with a different primary key has the same values for these fields.

This function exists to provide quick feedback to users of your application. It should not be relied on for any data guarantee as it has race conditions and is inherently unsafe. For example, if this check happens twice in the same time interval (because the user submitted a form twice), both checks may pass and you may end-up with duplicate entries in the database. Therefore, a unique_constraint/3 should also be used to ensure your data won’t get corrupted.

However, because constraints are only checked if all validations succeed, this function can be used as an early check to provide early feedback to users, since most conflicting data will have been inserted prior to the current validation phase.

Examples

unsafe_validate_unique(changeset, [:email], repo)
unsafe_validate_unique(changeset, [:city_name, :state_name], repo)
unsafe_validate_unique(changeset, [:city_name, :state_name], repo, message: "city must be unique within state")
unsafe_validate_unique(changeset, [:city_name, :state_name], repo, prefix: "public")
Link to this function update_change(changeset, key, function) View Source
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 validate_required/3).

Examples

iex> changeset = change(%Post{}, %{impressions: 1})
iex> changeset = update_change(changeset, :impressions, &(&1 + 1))
iex> changeset.changes.impressions
2
Link to this function validate_acceptance(changeset, field, opts \\ []) View Source
validate_acceptance(t(), atom(), Keyword.t()) :: t()

Validates the given parameter was given as true.

This validation is used to check for one specific parameter being true and as such does not require the field to effectively exist in the schema or the data being validated.

Options

  • :message - the message on failure, defaults to “must be accepted”

Examples

validate_acceptance(changeset, :terms_of_service)
validate_acceptance(changeset, :rules, message: "please accept rules")
Link to this function validate_change(changeset, field, validator) View Source
validate_change(
  t(),
  atom(),
  (atom(), term() ->
     [{atom(), String.t()} | {atom(), {String.t(), Keyword.t()}}])
) :: 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 :title, title  ->
...>   # Value must not be "foo"!
...>   if title == "foo" do
...>     [title: "cannot be foo"]
...>   else
...>     []
...>   end
...> end
iex> changeset.errors
[title: {"cannot be foo", []}]
Link to this function validate_change(changeset, field, metadata, validator) View Source
validate_change(
  t(),
  atom(),
  term(),
  (atom(), term() ->
     [{atom(), String.t()} | {atom(), {String.t(), Keyword.t()}}])
) :: 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]
Link to this function validate_confirmation(changeset, field, opts \\ []) View Source
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 if the confirmation field is nil or missing, by default this does not add a validation error. You can specify that the confirmation field is required in the options (see below). 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”
  • :required - boolean, sets whether existence of confirmation parameter is required for addition of error. Defaults to false

Examples

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

cast(data, params, [:password])
|> validate_confirmation(:password, message: "does not match password")
Link to this function validate_exclusion(changeset, field, data, opts \\ []) View Source
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))
Link to this function validate_format(changeset, field, format, opts \\ []) View Source
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/@/)
Link to this function validate_inclusion(changeset, field, data, opts \\ []) View Source
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)
Link to this function validate_length(changeset, field, opts) View Source
validate_length(t(), atom(), Keyword.t()) :: t()

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

Note that the length of a string is counted in graphemes. If using this validation to match a character limit of a database backend, it’s likely that the limit ignores graphemes and limits the number of unicode characters. Then consider using the :count option to limit the number of codepoints.

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
  • :count - what length to count for string, :graphemes (default) or :codepoints
  • :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)
Link to this function validate_number(changeset, field, opts) View Source
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 %{number}”
    • “must be greater than %{number}”
    • “must be less than or equal to %{number}”
    • “must be greater than or equal to %{number}”
    • “must be equal to %{number}”

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)
Link to this function validate_required(changeset, fields, opts \\ []) View Source
validate_required(t(), list() | atom(), Keyword.t()) :: t()

Validates that one or more fields are present in the changeset.

If the value of a field is nil or a string made only of whitespace, the changeset is marked as invalid and an error is added. Note the error won’t be added though if the field already has an error.

You can pass a single field name or a list of field names that are required.

Do not use this function to validate associations are required, instead pass the :required option to cast_assoc/3.

Options

  • :message - the message on failure, defaults to “can’t be blank”
  • :trim - a boolean that sets whether whitespaces are removed before running the validation on binaries/strings, defaults to true

Examples

validate_required(changeset, :title)
validate_required(changeset, [:title, :body])
Link to this function validate_subset(changeset, field, data, opts \\ []) View Source
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)