Ecto.Changeset.put_assoc
put_assoc
, go back to Ecto.Changeset module for more information.
Puts the given association entry or entries as a change in the changeset.
This function is used to work with associations as a whole. For example, if a Post has many Comments, it allows you to add, remove or change all comments at once. If your goal is to simply add a new comment to a post, then it is preferred to do so manually, as we will describe later in the "Example: Adding a comment to a post" section.
This function requires the associated data to have been preloaded, except
when the parent changeset has been newly built and not yet persisted.
Missing data will invoke the :on_replace
behaviour defined on the
association.
For associations with cardinality one, nil
can be used to remove the existing
entry. For associations with many entries, an empty list may be given instead.
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 values below, it will raise.
The associated data may be given in different formats:
a map or a keyword list representing changes to be applied to the associated data. A map or keyword list can be given to update the associated data as long as they have matching primary keys. For example,
put_assoc(changeset, :comments, [%{id: 1, title: "changed"}])
will locate the comment with:id
of 1 and update its title. If no comment with such id exists, one is created on the fly. Since only a single comment was given, any other associated comment will be replaced. On all cases, it is expected the keys to be atoms. Opposite tocast_assoc
andembed_assoc
, the given map (or struct) is not validated in any way and will be inserted as is. This API is mostly used in scripts and tests, to make it straight- forward to create schemas with associations at once, such as:Ecto.Changeset.change( %Post{}, title: "foo", comments: [ %{body: "first"}, %{body: "second"} ] )
changesets or structs - when a changeset or struct is given, they are treated as the canonical data and the associated data currently stored in the association is ignored. For instance, the operation
put_assoc(changeset, :comments, [%Comment{id: 1, title: "changed"}])
will send theComment
as is to the database, ignoring any comment currently associated, even if a matching ID is found. If the comment is already persisted to the database, thenput_assoc/4
only takes care of guaranteeing that the comments and the parent data are associated. This extremely useful when associating existing data, as we will see in the "Example: Adding tags to a post" section.
Once the parent changeset is given to an Ecto.Repo
function, all entries
will be inserted/updated/deleted within the same transaction.
Example: Adding a comment to a post
Imagine a relationship where Post has many comments and you want to add a
new comment to an existing post. While it is possible to use put_assoc/4
for this, it would be unnecessarily complex. Let's see an example.
First, let's fetch the post with all existing comments:
post = Post |> Repo.get!(1) |> Repo.preload(:comments)
The following approach is wrong:
post
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "bad example!"}])
|> Repo.update!()
The reason why the example above is wrong is because put_assoc/4
always
works with the full data. So the example above will effectively erase
all previous comments and only keep the comment you are currently adding.
Instead, you could try:
post
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "so-so example!"} | post.comments])
|> Repo.update!()
In this example, we prepend the new comment to the list of existing comments.
Ecto will diff the list of comments currently in post
with the list of comments
given, and correctly insert the new comment to the database. Note, however,
Ecto is doing a lot of work just to figure out something we knew since the
beginning, which is that there is only one new comment.
In cases like above, when you want to work only on a single entry, it is
much easier to simply work on the associated directly. For example, we
could instead set the post
association in the comment:
%Comment{body: "better example"}
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:post, post)
|> Repo.insert!()
Alternatively, we can make sure that when we create a comment, it is already associated to the post:
Ecto.build_assoc(post, :comments)
|> Ecto.Changeset.change(body: "great example!")
|> Repo.insert!()
Or we can simply set the post_id in the comment itself:
%Comment{body: "better example", post_id: post.id}
|> Repo.insert!()
In other words, when you find yourself wanting to work only with a subset
of the data, then using put_assoc/4
is most likely unnecessary. Instead,
you want to work on the other side of the association.
Let's see an example where using put_assoc/4
is a good fit.
Example: Adding tags to a post
Imagine you are receiving a set of tags you want to associate to a post. Let's imagine that those tags exist upfront and are all persisted to the database. Imagine we get the data in this format:
params = %{"title" => "new post", "tags" => ["learner"]}
Now, since the tags already exist, we will bring all of them from the database and put them directly in the post:
tags = Repo.all(from t in Tag, where: t.name in ^params["tags"])
post
|> Repo.preload(:tags)
|> Ecto.Changeset.cast(params, [:title]) # No need to allow :tags as we put them directly
|> Ecto.Changeset.put_assoc(:tags, tags) # Explicitly set the tags
Since in this case we always require the user to pass all tags
directly, using put_assoc/4
is a great fit. It will automatically
remove any tag not given and properly associate all of the given
tags with the post.
Furthermore, since the tag information is given as structs read directly from the database, Ecto will treat the data as correct and only do the minimum necessary to guarantee that posts and tags are associated, without trying to update or diff any of the fields in the tag struct.
Although it accepts an opts
argument, there are no options currently
supported by put_assoc/4
.