Search code examples
elixirecto

Dynamic number of inserts with Ecto.multi


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 Bars 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?


Solution

  • 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)