Hello,
Why doesn’t put_assoc
in the following test code update the parent entity?
# existing parent record
parent = Repo.get(Parent, 1) ||
Repo.insert!(%Parent{
:id => 1,
:text => "parent text"
})
# external data coming from a form to add a new "child"
child_attrs = %{
"text" => "child text",
"parent_id" => 1
}
changeset =
%Child{}
|> Child.changeset(child_attrs)
|> Ecto.Changeset.put_assoc(
:parent,
%{parent | text: "updated parent text"} # <- why doesn't the text get updated?
)
|> Repo.insert()
As you can see above, I am inserting a new entity and want to update the parent entity as well. However, in DB, the parent’s text field will remain as “parent text” and will not update to “updated parent text”.
The schema are as follow:
defmodule MyApp.Parent do
use Ecto.Schema
import Ecto.Changeset
schema "parent" do
field :text, :string
has_many :children, MyApp.Child
end
def changeset(collection, attrs) do
collection
|> cast(attrs, [:text])
|> validate_required([:text])
end
end
defmodule MyApp.Child do
use Ecto.Schema
import Ecto.Changeset
schema "child" do
field :text, :string
belongs_to :parent, MyApp.Parent
end
def changeset(collection, attrs) do
collection
|> cast(attrs, [:text, :parent_id])
|> validate_required([:text, :parent_id])
end
end
I tried to change
belongs_to :parent, MyApp.Parent
to
belongs_to :parent, MyApp.Parent, on_replace: :update
but the text still didn’t get updated.
Thank you for any help!
It won’t work like this.
You could probably use Ecto.Multi, or use an explicit update to parent. There are no callbacks in Ecto.
Why do You think a Repo.insert can update another record?
Repo.update()
can insert another record. For example, the following code in the doc using Repo.update()
will update and insert:
post
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "so-so example!"} | post.comments])
|> Repo.update!()
So in the same way, I thought that Repo.insert()
could potentially update a record through functions such as put_assoc()
. Also you there is an option you can set for belongs_to
named on_replace
. So why not?
What do you mean by that?
I mean no callback, as seen in Rails/Active Record callback…
Have you tried:
%{text: "updated parent text"}
or
%{id: child_attrs.parent_id , text: "updated parent text"}
Instead?
I usually do belongs_to :parent, MyApp.Parent, on_replace: :delete
but this is for a many-to-many relationship.
Also I do my put_assoc in my changesets.
53 def changeset(perfume_approval, attrs) do
54 company_records = id_records(:company_id, attrs["company_id"])
55 accord_records = id_records(:accord_id, attrs["accord_id"])
56 note_records = get_all_note_records(attrs)
57
58 perfume_approval
59 |> cast(attrs, [:perfume_name, :concentration, :gender, :perfume_description, :picture_url, :year_released, :month_released, :day_released, :submitter_user_id, :approved])
60 |> validate_required([:perfume_name, :gender, :concentration, :perfume_description, :submitter_user_id])
61 |> check_companies(company_records)
62 |> put_assoc(:perfume_approval_company_joins, company_records)
63 |> put_assoc?(:perfume_approval_accord_joins, accord_records)
64 |> put_assoc?(:perfume_approval_note_joins, note_records)
65 end
5 def put_assoc?(changeset, _atom, nil), do: changeset
6 def put_assoc?(changeset, atom, records) do
7 changeset
8 |> put_assoc(atom, records)
9 end
Have you tried:
%{text: "updated parent text"}
This will insert instead of an update. A new child and parent is created.
or
%{id: child_attrs.parent_id , text: "updated parent text"}
Will try to insert and will result in a constraint violation of the primary key.
But I don’t understand why; from the docs for the on_replace
option:
:update
- updates the association, available only for has_one
and belongs_to
. This option will update all the fields given to the changeset including the id for the association
and in the schema, I have set the option to “update”:
belongs_to :parent, MyApp.Parent, on_replace: :update
I come from PHP, never used Ruby. That’s why I didn’t get it:-)
Nevermind. That doesn’t work either. I understand what you’re trying to do now… yeah sorry good luck!
Try this:
I added in a list like the doc and insert! instead.
changeset =
%Child{}
|> Child.changeset(child_attrs)
|> Ecto.Changeset.put_assoc(
:parent,
[%{parent | text: "updated parent text"}]
)
|> Repo.insert!()
The example https://hexdocs.pm/ecto/Ecto.Changeset.html#put_assoc/4-example-adding-a-comment-to-a-post
have two ways it update! and insert!
Thank you anyway for having a look, it’s really appreciated. I’ll let you know if I find something out.
1 Like
Solved.
So the problem is that the attempts above such as:
|> Ecto.Changeset.put_assoc(
:parent,
%{parent | text: "updated parent text"}
)
don’t add any data into the changes of the changeset. Whatever you want to update, you must put it in the changeset’s changes. If you just pass a Schema structure, or a map with an ID, Ecto doesn’t know if this data exists in the database or not.
So something that I learned here, is that - if I’m not wrong! - Ecto knows whether it has to perform an insert or update for the assoc according to presence of data in changeset’s changes.
So the code needs to be something like:
parent_changeset =
Ecto.Changeset.change(parent, %{text: "updated parent text"})
%Child{}
|> Child.changeset(child_attrs)
|> Ecto.Changeset.put_assoc(:parent, parent_changeset)
|> Repo.insert!()
Use Ecto.Changeset.change() for internal/trusted data and Ecto.Changeset.cast() for external/untrusted data.
5 Likes
I want to add something here:
If I create a new Child (%Child{} |> Child.changeset(child_attrs) |> ...
), and want to update the associated parent, I need to pass a parent changeset as shown above (|> put_assoc(:parent, parent_changeset)
).
However, if I want to update an existing Child (child |> Child.changeset(child_attrs) |> ...
), and want to update the associated parent, I must first preload the parent (child |> Repo.preload(:parent) |> Child.changeset(child_attrs) |> ...
), and then in put_assoc
I must not pass a changeset, but I can simply pass a map (|> put_assoc(:parent, %{text: "updated parent text"}
), because the parent has been preloaded.
1 Like
Little correction to the last reply. To update the associated parent in the second case:
|> put_assoc(:parent, %{text: "updated parent text"}
should be:
|> put_assoc(:parent, %{id: 1, text: "updated parent text"}
Without passing the primary key (id), Ecto might think that you want to replace the parent by creating a new parent I guess.