Ecto.Changeset
Changesets allow filtering, casting and validation of model changes.
There is an example of working with changesets in the introductory
documentation in the Ecto
module.
The Ecto.Changeset struct
The fields are:
valid?
- Stores if the changeset is validrepo
- The repository applying the changeset (only set after a Repo function is called)model
- The changeset root modelparams
- The parameters as given on changeset creationchanges
- Thechanges
from parameters that were approved in castingerrors
- All errors from validationsvalidations
- All validations performed in the changesetrequired
- All required fields as a list of atomsoptional
- All optional fields as a list of atomsfilters
- Filters (as a map%{field => value}
) to narrow the scope of update/delete queries
Summary↑
add_error(changeset, key, error) | Adds an error to the changeset |
apply_changes(changeset) | Applies the changeset changes to the changeset model |
cast(model_or_changeset, params, required, optional \\ []) | Converts the given |
change(model_or_changeset, changes \\ %{}) | Wraps the given model in a changeset or adds changes to a changeset |
delete_change(changeset, key) | Deletes a change with the given key |
fetch_change(changeset, key) | Fetches a change from the given changeset |
fetch_field(changeset, key) | Fetches the given field from changes or from the model |
force_change(changeset, key, value) | Puts a change on the given |
get_change(changeset, key, default \\ nil) | Gets a change or returns a default value |
get_field(changeset, key, default \\ nil) | Gets a field from changes or from the model |
merge(changeset1, changeset2) | Merges two changesets |
put_change(changeset, key, value) | Puts a change on the given |
put_new_change(changeset, key, value) | Puts a change on the given |
update_change(changeset, key, function) | Updates a change |
validate_change(changeset, field, validator) | Validates the given |
validate_change(changeset, field, metadata, validator) | Stores the validation |
validate_confirmation(changeset, field, opts \\ []) | Validates that the given field matches the confirmation parameter of that field |
validate_exclusion(changeset, field, data, opts \\ []) | Validates a change is not included in given the enumerable |
validate_format(changeset, field, format, opts \\ []) | Validates a change has the given format |
validate_inclusion(changeset, field, data, opts \\ []) | Validates a change is included in the given enumerable |
validate_length(changeset, field, opts) | Validates a change is a string of the given length |
validate_number(changeset, field, opts) | Validates the properties of a number |
validate_unique(changeset, field, opts) | Validates the given |
Types ↑
t :: %Ecto.Changeset{valid?: boolean, repo: atom | nil, model: Ecto.Model.t | nil, params: %{String.t => term} | nil, changes: %{atom => term}, required: [atom], optional: [atom], errors: [error], validations: [{atom, String.t | {String.t, [term]}}], filters: %{atom => term}}
error :: {atom, error_message}
error_message :: String.t | {String.t, integer}
Functions
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
Specs:
- apply_changes(t) :: Ecto.Model.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)
Specs:
- cast(Ecto.Model.t | t, %{binary => term} | %{atom => term} | nil, [String.t | atom], [String.t | atom]) :: t
Converts the given params
into a changeset for model
keeping only the set of required
and optional
keys.
This functions 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.
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.
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.
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]
Specs:
- change(Ecto.Model.t | t, %{atom => term} | [Keyword.t]) :: t
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"
Specs:
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
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
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.
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
Specs:
Puts 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 = put_change(changeset, :title, "bar")
iex> changeset.changes
%{title: "bar"}
iex> changeset = put_change(changeset, :author, "bar")
iex> changeset.changes
%{title: "bar", author: "bar"}
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
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.
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!"
Specs:
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 ofchangeset2
in case of a conflict. If either changeset has its:params
field set tonil
, the resulting changeset will have its params set tonil
too.changes
- changes are merged giving precedence to thechangeset2
changes.errors
andvalidations
- they are simply concatenated.required
andoptional
- 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
Specs:
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"}
Specs:
Puts a change on the given key
only if a change with that key doesn’t
already exist, 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"}, %{})
iex> changeset = put_new_change(changeset, :title, "foo")
iex> changeset.changes
%{title: "foo"}
iex> changeset = put_new_change(changeset, :title, "bar")
iex> changeset.changes
%{title: "foo"}
iex> changeset = put_new_change(changeset, :author, "bar")
iex> changeset.changes
%{title: "foo"}
Specs:
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
Specs:
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}]
Specs:
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]
Specs:
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: "passwords do not match")
Validates a change is not included in given the enumerable.
Options
:message
- the message on failure, defaults to “is reserved”
Examples
validate_exclusion(changeset, :name, ~w(admin superadmin))
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/@/)
Validates a change is included in the given enumerable.
Options
:message
- the message on failure, defaults to “is invalid”
Examples
validate_inclusion(changeset, :gender, ["male", "female", "who cares?"])
validate_inclusion(changeset, :age, 0..99)
Specs:
Validates a change is a string of the given length.
Options
:is
- the string length must be exactly this value:min
- the string length must be greater than or equal to this value:max
- the string lenght must be less than or equal to this value:message
- the message on failure, depending on the validation, is one of:- “should be %{count} characters”
- “should be at least %{count} characters”
- “should be at most %{count} characters”
Examples
validate_length(changeset, :title, min: 3)
validate_length(changeset, :title, max: 100)
validate_length(changeset, :code, is: 9)
Specs:
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)
Specs:
Validates the given field
‘s uniqueness on the given repository.
The validation runs if the field (or any of the values given in scope) has changed and none of them contain an error. For this reason, you may want to trigger the unique validations as last in your validation pipeline.
Examples
validate_unique(changeset, :email, on: Repo)
Options
:message
- the message on failure, defaults to “has already been taken”:on
- the repository to perform the query on:downcase
- whentrue
, downcase values when performing the uniqueness query:scope
- a list of other fields to use for the uniqueness query
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.
These behaviours make it hard for Ecto to guarantee if the unique
validation is case insensitive or not and that’s why Ecto does not
provide a :case_sensitive
option.
However validate_unique/3
does provide a :downcase
option that
guarantees values are downcased when doing the uniqueness check.
When this option is set, values are downcased regardless of the
database being used.
Since the :downcase
option downcases the database values on the
fly, it should be used with care as it may affect performance. For example,
if this option is used, it could be appropriate to create an index with the
downcased value. Using Ecto.Migration
syntax, one could write:
create index(:posts, ["lower(title)"])
Many times, however, it’s simpler to just explicitly downcase values
before inserting them into the database and avoid the :downcase
option
in validate_unique/3
:
cast(params, model, ~w(email), ~w())
|> update_change(:email, &String.downcase/1)
|> validate_unique(:email, on: Repo)
Scope
The :scope
option allows specifying of other fields that are used to limit
the uniqueness check. For exmaple, if our use case limits a user to a single
comment per blog post, it would look something like:
cast(params, model, ~w(comment), ~w())
|> validate_unique(:user_id, scope: [:post_id], on: Repo)