8
votes

Changing the association of a given child from parent_a to parent_b via parent_id in params leaves a stale record.parent object.

e.g. (assume params matches %{child: %{id: '1', parent_id: '6'}})

# ...
child = Repo.get(Child, child_id)
|> preload([:parent])
changeset = Child.changeset(child, child_params)

case Repo.update(changeset) do
  {:ok, child} ->
    IO.puts child.parent_id # returns '6', or the new, changed `id`
    IO.puts child.parent.id # returns '5', or the old id
                            # child.parent is stale
# ...

What is the proper way to retrieve the newly associated parent record after updating?

2

2 Answers

6
votes

Force the preload. By default, Ecto won’t preload associations that are already loaded.

child
|> Child.changeset(params)
|> Repo.update!()
|> Repo.preload(:parent, force: true)

or without a bang update if you want to handle errors differently

child
|> Child.changeset(params)
|> Repo.update()
|> case do
  {:ok, child} -> {:ok, Repo.preload(child, :parent, force: true)}
  error -> error
end

In a more realistic example with error handling, it could look something like

with {:ok, child} <- get_child(child_id),
     {:ok, child} <- update_child(child, params) do
  # Do stuff
else
  {:error, %Ecto.Changeset{} = changeset} -> # Handle error
  {:error, reason} -> # Handle error
end

defp get_child(child_id) do
  case Repo.get(Child, child_id) do
    nil -> {:error, :not_found}
    child -> {:ok, child}
  end  
end

defp update_child(child, params) do
  updated_child = 
    child
    |> Child.changeset(params)
    |> Repo.update!()
    |> Repo.preload(:parent, force: true)
rescue
  error in Ecto.InvalidChangesetError -> {:error, error.changeset}
  error in RuntimeError -> {:error, error.message}
end
2
votes

There is no built in way to do this in Ecto for now. You also have the issue that you can't use preload, as the association has already been preloaded.

One option is this:

%{child | parent: Repo.get!(Parent, child.parent_id)}

You can also choose to not call preload until after you call Repo.update which will prevent the association already being loaded.