Ecto.Changeset.cast_assoc
cast_assoc
, go back to Ecto.Changeset module for more information.
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 receiving data external to the application.
cast_assoc/3
works matching the records extracted from the database
and compares it with the parameters received from an external source.
Therefore, it is expected that the data in the changeset has explicitly
preloaded the association being cast and that all of the IDs exist and
are unique.
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.get!(id)
|> Repo.preload(:addresses) # Only required when updating data
|> Ecto.Changeset.cast(params, [])
|> Ecto.Changeset.cast_assoc(:addresses, with: &MyApp.Address.changeset/2)
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.
Once cast_assoc/3
is called, Ecto will compare each parameter
with the user's already preloaded addresses and act as follows:
- If the parameter does not contain an ID, the parameter data
will be passed to
MyApp.Address.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
MyApp.Address.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
MyApp.Address.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 MyApp.Address.changeset/2
function is invoked, it must
return a changeset. Once the parent changeset is given to an Ecto.Repo
function, all entries will be inserted/updated/deleted within the same
transaction.
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(params, [:title, :body])
|> validate_required([: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
Partial changes for many-style associations
By preloading an association using a custom query you can confine the behavior
of cast_assoc/3
. This opens up the possibility to work on a subset of the data,
instead of all associations in the database.
Taking the initial example of users having addresses imagine those addresses are set up to belong to a country. If you want to allow users to bulk edit all addresses that belong to a single country, you can do so by changing the preload query:
query = from MyApp.Address, where: [country: ^edit_country]
User
|> Repo.get!(id)
|> Repo.preload(addresses: query)
|> Ecto.Changeset.cast(params, [])
|> Ecto.Changeset.cast_assoc(:addresses)
This will allow you to cast and update only the association for the given country. The important point for partial changes is that any addresses, which were not preloaded won't be changed.
Options
: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":force_update_on_change
- force the parent record to be updated in the repository if there is a change, defaults totrue
:with
- the function to build the changeset from params. Defaults to thechangeset/2
function of the associated module. It can be changed by passing an anonymous function or an MFA tuple. If using an MFA, the default changeset and parameters arguments will be prepended to the given args. For example, usingwith: {Author, :special_changeset, ["hello"]}
will be invoked asAuthor.special_changeset(changeset, params, "hello")