Cast Assoc code

cast_assoc(changeset, name, opts \ [])

Casts the given association with the changeset params

This function should be used when working withe 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 parameter 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 follow

1
2
3
4
%{"name" => "John Doe", "addresses" => [
%{"street" => "Some where", "country" => "Brzil", "id" => 1},
%{"street" => "Else where", "country" => "Poland"},
]}

and then

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

Once cast_assoc/3 is called, Ecto will compare those params with the addresses already associated with the user and acts 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 children 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

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
`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`

Furthurmore, if each side of the association is managed seperately, it is prefereable 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" => ["linear"]}

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 called.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def cast_assoc(changeset, name, opts \\ []) when is_atom(name) do
cast_relation(:assoc, changeset, name, opts)
end

defp cast_relation(type, %Changeset{} = changeset, key, opts) do
{ key, param_key } = cast_key(key)
%{data: data, types: types, params: params, changes, changes } = changeset
%{related: related} = relation = relation!(:cast, type, key, Map.get(types, key))
params = params || %{}

{changeset, required?} = if opts[:required] do
{update_in(changeset.required, &[key|&1]), true}
else
{changeset, false}
end

on_cast = Keyword.get_lazy(opts, :with, fn -> on_cast_default(type, related) end)
original = Map.get(data, key)

changeset = case Map.fetch(params, params_key) do
{:ok, value} ->
current = Relation.load!(data, original)
case Relation.cast(relation, value, current, on_cast) do
{:ok, change, relation_valid?} when change != original ->
missing_relation(%{changeset | change: Map.put(changes, key, changes), valid?: changeset.valid? and relation_valid?}, key, current, required?, relation, opts)
:error ->
%{changeset | errors: [{key, {message(opts, :invalid_message, "is invalid"), [type: expected_relation_type(relation)]}} | changeset.errors], valid? false}
_ -> missing_relation(changeset, key, current, required?, relation, opts)
end
:error -> missing_relation(changeset, key, current, required?, relation, opts)
end

update_in changeset.types[key], fn {type, relation} ->
{type, %{relation | on_cast: on_cast}}
end
end

Put Assoc

As alternative to cast_assoc/3

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

If each side of the association is managed seperately, 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.

1
%{"name" => "John Doe", "tags" => ["lieanr"]}

and then:

1
2
3
4
5
6
tags = Repo.all(from t in Tag, where: t.name in ^params["tags"])

user
|> Repo.preload(:tags)
|> Ecto.Changeset.cast(params)
|> Ecto.Changeset.put_assoc(:tags, tags)

Example

Build_Assoc

Gen an instance with foreign key

1
2
3
4
5
6
iex> post = Ecto.build_assoc(user, :posts, %{header: "Clickbait header", body: "No real contet"})
%EctoAssoc.Post{__meta__: #Ecto.Schema.Metadata<:built, "posts">,
body: "No real content", header: "Clickbait header", id: nil,
user: #Ecto.Association.NotLoaded<association :user is not loaded>, user_id: 1}

iex> Repo.insert!(post)
Put_Assoc

Add association to changeset which is not persisted

1
2
3
iex> post_changeset = Ecto.Changeset.change(post)
iex> post_with_tags = Ecto.Changeset.put_assoc(post_changeset, :tags, [misc_tag])
iex> Repo.insert!(post_with_tags)

Conclusion

cast_assoc when you want to cast external parameters, like the ones from a form, into an association.

put_assoc when you already have an association struct

build_assoc receive an existing struct(for example user), that was persisted to the database, and builds a struct(for example post, based on its association(for example :posts), with the foreign key field(for example user_id).