Ecto Reload belongs_to Association After Successful Repo.update
Asked Answered
U

2

8

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?

Uropygium answered 22/1, 2016 at 11:21 Comment(0)
H
2

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.

Hannibal answered 22/1, 2016 at 11:21 Comment(2)
With Ecto 2.0, you can now pass force: true to Repo.preload to force reloading of an already-loaded association. Source: hexdocs.pm/ecto/2.0.0-rc.1/Ecto.Repo.html#c:preload/3Amentia
@JonathanPerret worth turning your comment into an answer? In modern versions of Ecto, this seems to be the idiomatic way of solving the problem.Ivette
L
8

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
Labuan answered 5/2, 2019 at 22:32 Comment(0)
H
2

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.

Hannibal answered 22/1, 2016 at 11:21 Comment(2)
With Ecto 2.0, you can now pass force: true to Repo.preload to force reloading of an already-loaded association. Source: hexdocs.pm/ecto/2.0.0-rc.1/Ecto.Repo.html#c:preload/3Amentia
@JonathanPerret worth turning your comment into an answer? In modern versions of Ecto, this seems to be the idiomatic way of solving the problem.Ivette

© 2022 - 2024 — McMap. All rights reserved.