17
votes

I'm trying to insert an invoice struct along with its associated invoice items. I'm able to insert the invoice data, and call an anonymous function to validate, cast, and insert each item. Since insert/2 does not produce a return, how can I get the invoice_id for the items while still being able to roll back the entire transaction if one item fails validation or insertion?

I've put the code in my own repo, here it is:

def insertassoc(params) do
 Repo.transaction(fn ->    
  i = Invoice.changeset(params["data"], :create)
    if i.valid? do
      Repo.insert(i)
    else
      Repo.rollback(i.errors)
    end

  insert_include = fn k ->
    c = InvoiceItem.changeset(k, :create)
    if c.valid? do
      Repo.insert(c)
    else
      Repo.rollback(c.errors)
    end
  end

  for include <- params["includes"] do
    insert_include.(Map.merge(include, %{"invoice_id" => ????}))
  end

 end)
end

and here is how I use it from my controller:

def create(conn, params) do
 case InvoiceRepo.insertassoc(params) do
  {:ok, x} ->
    json conn, Map.merge(params, %{"message" => "OK"})
  {:error, x} ->
    json conn |> put_status(400), Map.merge(params, %{"message" 
    => "Error"})
 end
end

There aren't many up to date examples out there with Ecto, so sorry if these are noob questions ;-). Anyone have an idea? I tried putting the invoice insert in a private function, and using a case block to determine whether the main transaction should roll back, but I couldn't figure out how to get the invoice id back from that either.

2

2 Answers

28
votes

Repo.insert/1 actually returns the model you have just inserted. You also want to decouple the validation from the transaction handling as much as possible. I would suggest something as follows:

invoice = Invoice.changeset(params["data"], :create)
items   = Enum.map(params["includes"], &InvoiceItem.changeset(&1, :create))

if invoice.valid? && Enum.all?(items, & &1.valid?) do
  Repo.transaction fn ->
    invoice = Repo.insert(invoice)
    Enum.map(items, fn item ->
      item = Ecto.Changeset.change(item, invoice_id: invoice.id)
      Repo.insert(item)
    end)
  end
else
  # handle errors
end
10
votes

In Ecto 2.0 you would be doing something like:

%My.Invoice{}
|> Ecto.Changeset.change
|> Ecto.Changeset.put_assoc(:invoice_items, [My.InvoiceItem.changeset(%My.InvoiceItem{}, %{description: "bleh"})])
|> My.Repo.insert!

(The accepted answer works pre 2.0, also, Valim mentions in the comments of that answer of the existence of put_assoc)