I have a Foo
schema in my app that has_many
of the schema Bar
.
schema "foos" do
field :content, :string
has_many :bars, MyApp.Content.Bar, foreign_key: :foo_id
end
schema "bars" do
field :content, :string
belongs_to :foo, MyApp.Content.Foo, foreign_key: :foo_id
end
I want a function that takes an id for a Foo
, creates a copy of that Foo
and inserts it, and then creates copies of all the associated Bar
s with the new Foo
. Given a Foo with many child Bars, this function will copy that Foo and all those Bars in one go.
I use Ecto.Multi for things like this, but I'm not sure how to set it up for a variable number of actions. So far I have this:
resolve fn (%{foo_id: foo_id}, _info) ->
oldfoo = Repo.get!(Foo, foo_id)
multi =
Multi.new
|> Multi.run(:foo, fn %{} ->
MyApp.Content.create_foo(%{
content: oldfoo.content
}) end)
|> Multi.run(:bars, fn %{foo: foo} ->
query =
from b in Bar,
where: b.foo_id == ^foo.id,
select: b.id
bars = Repo.all(query) # returns a list of id/ints like [2,3,6,7,11...]
Enum.map(bars, fn barid ->
bar = Repo.get(Bar, barid)
Bar.changeset(%Bar{}, %{
content: bar.content,
foo_id: foo.id
})
|> Repo.insert()
end)
end)
case Repo.transaction(multi) do
{:ok, %{foo: foo}} ->
{:ok, foo}
{:error, _} ->
{:error, "Error"}
end
This throws an error:
** (exit) an exception was raised:
** (CaseClauseError) no case clause matching: [ok: %MyApp.Content.Foo...
Is there a reasonable way to do this within Ecto.Multi?
I'd use Enum.reduce_while/3
here. We loop over the list, collecting all the inserted bars. If any insert fails, we return that error, otherwise we return the collected values in a list.
Enum.reduce_while(bars, {:ok, []}, fn barid, {:ok, acc} ->
bar = Repo.get(Bar, barid)
Bar.changeset(%Bar{}, %{
content: bar.content,
foo_id: foo.id
})
|> Repo.insert()
|> case do
{:ok, bar} -> {:cont, {:ok, [bar | acc]}}
{:error, error} -> {:halt, {:error, error}}
end
end)