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

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

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
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews