I’m trying to use Ecto’s new :sort_param attribute on a cast_assoc to apply the sort order I specified in my form. Since I’m using a belongs_to relationship, I’m doing this in two steps as specified in the docs:
changeset
|> cast_assoc(:element_photos,
sort_param: :element_photos_order
)
|> reorder_photos()
...
defp reorder_photos(changeset) do
if element_photos = Ecto.Changeset.get_change(changeset, :element_photos) do
element_photos
|> Enum.with_index(fn element_photo, index ->
Ecto.Changeset.put_change(element_photo, :order, index)
end)
|> then(&Ecto.Changeset.put_change(changeset, :element_photos, &1))
else
changeset
end
end
But this always fails for me:
** (RuntimeError) cannot replace related %Ido.Elements.ElementPhoto{...}. This
typically happens when you are calling put_assoc/put_embed with the results of a
previous put_assoc/put_embed/cast_assoc/cast_embed operation, which is not
supported. You must call such operations only once per embed/assoc, in order
for Ecto to track changes efficiently
(ecto 3.10.0) lib/ecto/changeset/relation.ex:430: Ecto.Changeset.Relation.check_action!/2
(ecto 3.10.0) lib/ecto/changeset/relation.ex:180: Ecto.Changeset.Relation.do_change/4
(ecto 3.10.0) lib/ecto/changeset/relation.ex:347: Ecto.Changeset.Relation.map_changes/9
(ecto 3.10.0) lib/ecto/changeset/relation.ex:156: Ecto.Changeset.Relation.change/3
(ecto 3.10.0) lib/ecto/changeset.ex:1729: Ecto.Changeset.put_change/7
(ecto 3.10.0) lib/ecto/changeset.ex:1720: Ecto.Changeset.put_change/3
How do I workaround this? I tried replacing that last put_change with an update_change, but I get the same error. I’m not sure what to do here, as the docs specifically suggest using cast_assoc and then put_change like this.
Just an idea, but maybe try swapping get_change for get_field to avoid calling a put_change on the children after the cast_assoc.
defp reorder_photos(changeset) do
if element_photos = Ecto.Changeset.get_field(changeset, :element_photos) do
element_photos
|> Enum.with_index(fn element_photo, index -> %{element_photo | order: index} end)
|> then(&Ecto.Changeset.put_change(changeset, :element_photos, &1))
else
changeset
end
end
Also, I wonder if flipping the pipeline would make a difference.
Hi codeanpeace, thanks for the ideas! Sadly those won’t work either.
If I use get_field instead of get_change, then I’ll always be putting the update in the changeset regardless of whether there was a change. Then I’ll still hit the issue of Ecto not wanting it to be changed twice.
I can’t flip the order in the pipeline, as the cast_assoc is needed to pull the data out of the attrs and update the order based on the sort param.
You can use the changeset from before you cast_assoc, while still using the one from after cast_assoc to build the put_change payload (which maybe should be put_assoc?)
Oof amazingly that doesn’t work either. It also inspired me to try changeset |> delete_change(:element_photos) |> put_change(:element_photos, element_photos) which also fails. I’m guessing the failure is actually because of the nested Changesets- it sees you’re trying to set a change that contains Changesets, and decides you already did that and it’s not allowed.
All these attempts also feeling like I’m fighting against Ecto- I’m not sure what the elegant proper way to do this is.
I felt similarly when brainstorming possible solutions.
Yeah, the error message shows the nested %ElementPhoto{} struct which is why I was curious if swapping the get_change and nested put_change with a get_field and nested struct update would make a difference.
A potential temporary solution could be using the list :element_photos_order passed into :sort_param option as a dictionary to directly add/update the :order keys when Enum.maping through the association in the changeset before casting the association. It does still feel a bit roundabout though.
Yeah, I think the only way to do this would be outside of the changeset. Otherwise anything I do or anything the changeset does will collide. I was already handling sort manually like this, so I’m just reverting my attempt with :sort_param until there’s another approach in Ecto. Thanks for the help!