omin

omin

I have a transaction that updates upto 3 tables and it became long and ugly. I’d love to learn more to be able to write cleaner and more efficient code.

I have a form that takes inputs to update upto 3 tables.

a. Uses external API to cache the data if it expired or doesn’t already exist.
b. build_assoc with (a) for a new data point
c. build_assoc with (a) and (b) for a new data point

Some specific questions:

  1. Since transactions should be kept as short as possible, would you recommend fetching from the external API outside the transaction? (This is what I’ve done but it adds complexity to the code.)
  2. What would be to best practice to manage layers of associations? I found http://stackoverflow.com/questions/38033817/how-to-make-forms-and-transactions-play-well-in-phoenix-ecto/39415888#39415888 and I’m leaning toward the pattern matching example.
  3. How would you rollback multiple changesets?

Showing Posts 1 to 9

josevalim

josevalim

Creator of Elixir

It seems Ecto.Multi may be exactly what you need: Ecto.Multi — Ecto v3.14.0

It answers questions 1 and 3 and it may as well answer question 2 too.

omin

omin OP

Maybe. I’ve looked into it but haven’t found a way to apply associations yet.

josevalim

josevalim

Creator of Elixir

Since insert/update in Ecto.Multi accepts changesets, you can still build the associations in the changesets if you want to. If you can build everything with associations, then you don’t need multi because the transaction part is taken care for you when you try to introduce the changeset.

omin

omin OP

Thanks for the reply @jose.

Do you think we can go through an example? I couldn’t find anything online :sweat:

  transaction = Repo.transaction fn ->
    location = location || Repo.insert!(location_changeset)

    item_name = photo_params["item_name"]
    item = Repo.get_by(App.Item, [name: item_name, location_id: location.id])

    item_changeset = 
      location
      |> build_assoc(:items)
      |> App.Item.changeset(Map.put(photo_params, "name", item_name))
    item = item || Repo.insert!(item_changeset)

    photo_changeset =
      location
      |> build_assoc(:photos, item_id: item.id, user_id: current_user.id)
      |> Photo.changeset(photo_params)
    Repo.insert(photo_changeset)
  end
  
  case transaction do
    {:ok, _photo} ->
      conn
      |> redirect(to: phto_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end

Would ecto rollback everything when the second or third Repo.insert fails? Also, is there a way to cascade a changeset with multiple schemas?

dimitarvp

dimitarvp

Almost two years later: yes, according to the official docs any exception raised inside the function given to Repo.transaction will result in a rollback.

Please note though, the third insert in your code is not using the bang variant of the function (namely it’s not insert!) and will thus not raise an exception so the transaction will likely still succeed with only partial success – not good.

EDIT: The above is NOT true: Ecto.Multi docs

To use Repo.transaction, do one of these:

  1. Either use bang functions everywhere in the transaction function (Repo.insert!, Repo.update! etc.), or…
  2. Use the with keyword and chain all the operations through non-bang functions and call Repo.rollback in the else clause – which would mean that the first failed operation will return {:error, reason} and the transaction will return that exact error so you can troubleshoot further afterwards. Or…
  3. Just use Ecto.Multi which will give you even more info if an operation fails. It’s really the best way of doing such composite operations ever since it was introduced. Just have one Ecto.Multi variable and append all your operations to it, then just execute it at once: Repo.transaction(your_multi).

It’s best if you don’t mix these styles. Just pick one and stick with it.

I started off using more hacky solutions and trying to be clever but nowadays I am always using Ecto.Multi and I am very pleased with the results. The code is much easier for a human to understand as well, which is a huge bonus win.

michalmuskala

michalmuskala

This is not possible - the transaction will always either fully succeed or fully fail. If there’s a query inside a transaction that failed, but not raised the Repo.transaction block will return a generic error {:error, :rollback}.

dimitarvp

dimitarvp

Apologies, maybe I was misled by the documentation. I cannot see there explicitly stated that if a function returns {:error, reason} tuple then the transaction will be rolled back.

Maybe this part?

A successful transaction returns the value returned by the function wrapped in a tuple as {:ok, value}.

Or if not, can you point me at the docs that show my mistake?

tcoopman

tcoopman

dimitarvp

dimitarvp

Thank you. I only looked at the Repo.transaction docs. Edited my post above, don’t want to mislead people.

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews