Hello !
I’m a newcomer in the Elixir / Ecto / Phoenix world, and having an issue with how to handle associations in Ecto.
Considering the following (simplified) related schemas and changesets:
schema "base"
field :reference, :string
field :designation, :string
belongs_to :user, User
has_many :base_components, BaseComponent, on_replace: :delete
end
def changeset(base, attrs) do
base
|> cast(attrs, [
:reference,
:designation
])
|> Changeset.cast_assoc(
:base_components,
required: true
)
|> validate_length(
:components,
min: 2,
)
end
schema "base_components"
field :quantity, :decimal
belongs_to :user, User
belongs_to :base, Base
end
def changeset(base_component, attrs) do
base_component
|> cast(attrs, [
:quantity
])
end
I’m basically trying to insert / update a Base
along with it’s related BaseComponent
's (enforcing a minimum of 2 to be provided). Using the following context function body (both user
and attrs
are provided in the function params) for insert:
%Base{}
|> Base.changeset(attrs)
|> Changeset.put_change(:user_id, user.id)
|> Changeset.update_change(:base_components, fn changesets ->
changesets |> Enum.map(&Changeset.put_change(&1, :user_id, user.id))
end)
and this one for update:
base
|> Repo.preload(:base_components)
|> Base.changeset(attrs)
|> Changeset.update_change(:base_components, fn changesets ->
changesets |> Enum.map(&Changeset.put_change(&1, :user_id, user.id))
end)
Now, on insert, everything goes according to plan, but when updating, i’m having an issue (while unit testing). When the cast_assoc
highlights that some BaseComponent
will be deleted, others updated and some inserted (usually between 2 to 5 base components are provided), i get the following error:
** (RuntimeError) cannot replace related %MyApp.MyContext.BaseComponent{} because it already exists
and it is not currently associated with the given struct. Ecto forbids casting existing records through
the association field for security reasons. Instead, set the foreign key value accordingly
From what i understand, it comes from the presence of a changeset marked to be deleted, which action
isn’t permitted to be changed. Note that if i add the put_change
directly on the changeset for BaseComponent
(right after cast
) providing the user params to the changeset function, it works (probably because it’s not applied on the deleted associations).
Is there a way to do what i want (using put_change
), without having to do it in the changeset function (keeping those changes in the context function and avoiding having internal data in changeset functions which i want to stay strictly related to user provided data) ?
Thanks