Ecto.Changeset.put_assoc

You're seeing just the function put_assoc, go back to Ecto.Changeset module for more information.
Link to this function

put_assoc(changeset, name, value, opts \\ [])

View Source

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 to cast_assoc and embed_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 the Comment 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, then put_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.