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 - when changesets are given, they are treated as the canonical
data and the associated data currently stored in the association is either
updated or replaced. For example, if you call
`put_assoc(post_changeset, :comments, [list_of_comments_changesets])`,
all comments with matching IDs will be updated according to the changesets.
New comments or comments not associated to any post will be correctly
associated. Currently associated comments that do not have a matching ID
in the list of changesets will act according to the `:on_replace` association
configuration (you can chose to raise, ignore the operation, update or delete
them). If there are changes in any of the changesets, they will be
persisted too.
* structs - when structs are given, they are treated as the canonical data
and the associated data currently stored in the association is replaced.
For example, if you call
`put_assoc(post_changeset, :comments, [list_of_comments_structs])`,
all comments with matching IDs will be replaced by the new structs.
New comments or comments not associated to any post will be correctly
associated. Currently associated comments that do not have a matching ID
in the list of changesets will act according to the `:on_replace`
association configuration (you can chose to raise, ignore the operation,
update or delete them). Different to passing changesets, structs are not
change tracked in any fashion. In other words, if you change a comment
struct and give it to `put_assoc/4`, the updates in the struct won't be
persisted. You must use changesets instead. `put_assoc/4` with structs
only takes care of guaranteeing that the comments and the parent data
are associated. This is extremely useful when associating existing data,
as we will see in the "Example: Adding tags to a post" section.
according to above.
I can update specific column even if there is a changeset or map that I put in the second parameter of put_assoc with the same pk of the parent table.
so, I tested it.
defmodule ConstraintTest.Schema.Post do
use Ecto.Schema
import Ecto.Changeset
alias ConstraintTest.Schema.Content
schema "posts" do
field :title, :string
field :content, :string
has_many :contents, Content
timestamps()
end
def changeset(struct_or_changeset, attrs) do
struct_or_changeset
|> cast(attrs, [:title, :content])
end
end
defmodule ConstraintTest.Schema.Content do
use Ecto.Schema
import Ecto.Changeset
alias ConstraintTest.Schema.Post
@timestamps_opts [type: :utc_datetime]
schema "contents" do
field :reply, :string
field :nick, :string
field :age, :integer
field :email, :string
belongs_to :post, Post
timestamps()
end
def changeset(struct_or_changeset, attrs) do
struct_or_changeset
|> cast(attrs, [:reply, :nick, :age, :email])
|> validate_required([:reply, :nick])
end
end
defmodule Context do
import Ecto.Query, warn: false
alias ConstraintTest.Repo
alias ConstraintTest.Schema.{Post, Content}
def create_content(post, attrs) do
# 1. map
# contents = attrs
# 2. changeset
contents =
Enum.map(attrs, fn attr ->
Ecto.Changeset.change(%Content{}, attr)
end)
post = Repo.preload(post, :contents)
post
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:contents, contents ++ post.contents)
|> Repo.update()
end
end
post = %{
"title" => "My first post",
"content" => "Lorem ipsum dolor sit amet consectetur adipisicing elit. Natus, aliquam voluptatibus modi sunt similique aspernatur veritatis maiores autem alias aut consectetur sint illum accusamus blanditiis eos quidem tempore excepturi veniam."
}
{:ok, post_struct} = Context.create_post(post)
contents = [
%{
reply: "first comment",
nick: "follower",
age: 30,
email: "follower@fan.com"
}, %{
reply: "second comment",
nick: "enemy",
age: 33,
email: "enemy@fan.com"
}, %{
reply: "third comment",
nick: "none",
age: 10,
email: "none@business.com"
}
]
{:ok, post_struct} = Context.create_content(post_struct, contents)
list_of_comments = [
%{
id: 1,
reply: "fourth comment",
nick: "follower1"
}, %{
id: 2,
reply: "fifth comment",
nick: "enemy1"
}, %{
id: 3,
reply: "sixth comment",
nick: "none1"
}
]
Context.create_content(updated_post, list_of_comments)
result that i expect. becase they are updated.
%{
id: 1,
reply: “fourth comment”,
nick: “follower1”,
age: 30,
email: “follower@fan.com”
}, %{
id: 2,
reply: “fifth comment”,
nick: “enemy1”,
age: 33,
email: “enemy@fan.com”
}, %{
id: 3,
reply: “sixth comment”,
nick: “none1”
age: 10,
email: “none@business.com”
}
but got
** (Ecto.ConstraintError) constraint error when attempting to insert struct:
* contents_pkey (unique_constraint)
If you would like to stop this constraint violation from raising an
exception and instead add it as an error to your changeset, please
call unique_constraint/3
on your changeset with the constraint
:name
as an option.
The changeset has not defined any constraint.
(ecto 3.9.4) lib/ecto/repo/schema.ex:795: anonymous fn/4 in Ecto.Repo.Schema.constraints_to_errors/3
(elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ecto 3.9.4) lib/ecto/repo/schema.ex:780: Ecto.Repo.Schema.constraints_to_errors/3
(ecto 3.9.4) lib/ecto/repo/schema.ex:761: Ecto.Repo.Schema.apply/4
(ecto 3.9.4) lib/ecto/repo/schema.ex:369: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(ecto 3.9.4) lib/ecto/association.ex:815: Ecto.Association.Has.on_repo_change/5
(ecto 3.9.4) lib/ecto/association.ex:573: anonymous fn/8 in Ecto.Association.on_repo_change/7
(elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
here is my question
what if i am using
|> Ecto.Changeset.put_assoc(:contents, contents)
instead of
|> Ecto.Changeset.put_assoc(:contents, contents ++ post.contents)
→ on_replace option is needed!
I want to use :update option but can’t cause this is has_many relation.
so, nilify or delete is available but the result is not what i expect neither.
Any other way i can update?
How can I update columns with the same pk by using put_assoc?