dimitarvp

dimitarvp

Seeking thoughts on advantages of the Repo.transact pattern vs disadvantages I’ve read about Ecto.Multi

Hello,

I am looking to more closely understand the advantages of the Repo.transact pattern and the quoted disadvantages of Ecto.Multi.

Repo.transact is quickly described in this blog post.

(Link to the original article by Sasa Juric is included in the post above.)

Here are four (4) points from the article, and my comments. I am quoting @sasajuric and @tomkonidas.


This function commits the transaction if the lambda returns {:ok, result}, rolling it back if the lambda returns {:error, reason}. In both cases, the function returns the result of the lambda.

Does Ecto.Multi ultimately not do that as well? Sure it’s not a direct response; in case of errors you get a tuple containing various state, including “changes so far”. I’d think that the rich choices offered by Ecto.Multi are welcome – you get to decide for yourself how do you want to react to problems. How is the Repo.transact feature the clear winner here?

One thing I can see is if we unify our error-handling code by utilizing shared helpers + including the Repo.transact in an even higher-level wrappers (business code). Is that the selling point – less boilerplate?


We chose this approach over Ecto.Multi, because we’ve experimentally established that multi adds a lot of noise with no real benefits for our needs.

I am very curious about this empirical evidence; I think having it spelled out somewhere would be hugely valuable both for new learners and more long-term users like myself. I can only imagine it’s the proposition that the else clauses of the with statement are hard to maintain? Or having to know the changeset functions (which I don’t view to be as negative thing as the blog post author seems to imply)?


As we can see, it is not the worst, but once we see the Repo.transact/2 way, it will be clear which is better.

It’s clear only insofar as the code is (1) shorter and definitely easier to read, and (2) has no else clause(s). Is that all? I am all for shorter and more readable code, I am almost religious about it too, but not all teams are open to code modifications on that basis alone. I am looking to understand if there’s more to this beyond code readability.

Perhaps the error-handling utilities we can combine with Repo.transact are the true value proposition here?


Another big benefit is that we do not need to go down to the changeset level for inserting, we could use our functions that perform Repo.inserts in them (Accounts.new_user_changeset/1 vs Accounts.create_user/1). This lets us compose many functions together from outside the context modules without having the need to expose your changeset functions.

Hmmm. I’ve been in 3 big-ish Elixir codebases (we’re talking 1000 - 3000 files) and I have never stumbled upon a problem that we the team would describe as “modules outside the Phoenix contexts have access to changeset functions and that is a problem”. I mean they are public; being able to call them is always on the table, this is not Java / C# / Rust et. al. where you can make functions (package|namespace)-private so you can limit who can actually call them.

A notable mention here is the boundaries library, made by Sasa Juric as well: Boundary - enforcing boundaries in Elixir projects. I’ve used it with success and I do like it but sadly I never managed to convince too many people of its value.

I am not sure I see the clear win here. Help me understand.


Maybe the actual root problem is that I worked as a contractor for several years and had to be very flexible – meaning that things that many of us as programmers would immediately agree on became actual contention points when working with different teams and CTOs. Or maybe I am just a bad developer advocate (would not surprise me :smiley:).

My summary of the advantages of Repo.transact would be (1) less boilerplate and (2) better separation of concerns. I respect and even worship both but I’ve met plenty of people who don’t so I am curious how would I sell them such a coding pattern better and be more convincing.

In any case: I am very curious as to why is the Repo.transact pattern deemed valuable (or not) by others. What other factors did I miss? Could you share your thoughts, please? Thank you.

Most Liked

sasajuric

sasajuric

Author of Elixir In Action

I wrote Repo.transact after seeing a lot of production code along the lines of what’s written in that excellent blog post by @tomkonidas.

The value proposition of Repo.transact is that control flow features such as passing data around, branching, early exit, can be implemented with standard Elixir features, such as variables, functions, and the with expression. The transactional logic is less special, and it doesn’t rely on some implicit behaviour of a function from some library.

Combined with the provable fact that the transact code is shorter (often significantly), even in such simple example as in that blog post, I have no doubt that the transact version is simpler and clearer.

That’s not to say that Multi is universally bad. The ability to provide each db operation as data is definitely interesting, and could be useful in the cases where the transactional steps need to be assembled dynamically (perhaps provided by the client code). But in the vast majority of cases I’ve encountered, I find the multi code needlessly difficult to read. This is true even in simple cases, and it becomes progressively worse if the transactional logic is more involved (e.g. if it requires branching early on in the transaction).

Hence, I strongly prefer transact, and it’s what I advise using in most situations.

27
Post #2
wojtekmach

wojtekmach

Hex Core Team

Repo.transact/2 was just released in Ecto v3.13.0.

wojtekmach

wojtekmach

Hex Core Team

I have created a PR for Repo.transaction_with:

Thanks everyone for feedback, in particular @sasajuric and @tomkonidas for writing about this.

sasajuric

sasajuric

Author of Elixir In Action

Yeah, this is the gist of Repo.transact. It just uses ok/error for commit/rollback instead of throwing exceptions. As a result, the code in the provided lambda can be expressed as a with chain, as demonstrated in the blog post. Let’s take a look at a slightly modified version of the blog post:

with {:ok, user} <- Repo.insert(user_data),
     {:ok, _log} <- Repo.insert(%LogData{user_id: user.id, ...}),
     ...,
     do: {:ok, user}

This is an equivalent of the blog sketch, but with wrapper functions removed, to make things more obvious.

To make this sequence transactional, all you have to do is place Repo.transact(fn -> ... end) around it. In other words, you can use vanilla Elixir to express the logical flow of the operation.

In contrast, Multi uses a custom mechanism of operation chaining. We do something like:

Multi.new()
|> Multi.insert(:user, user_changeset)
|> Multi.insert(:log, fn %{user: user} -> %LogData{user_id: user.id, ...} end)
|> ...

The fact that the 2nd insert is performed only if the 1st one succeeds is now not so obvious. It is a custom special behaviour of the library code (presumably handled by Repo.transaction).

So control-flow mechanism is now special. Outside of db transactions we use vanilla Elixir. Inside, we use the custom mechanism of multi. This is confusing. Why do I have to use different approaches to early-exit depending on whether I’m inside an Ecto transaction or not?

Furthermore, the plain multi operations such as insert & co can only go so far. Often you’ll need to fallback to run. Consider the following example:

with {:ok, user} <- Repo.insert(user_data) do
  Repo.insert!(%LogData{user_id: user.id, ...})
  ...
  {:ok, user}
end

This version is more precise about which parts can return an error to the caller, vs which parts should always succeed. If inserting a user record fails, we’ll return the error, which will typically be forwarded to the external client (e.g. browser). OTOH, if we fail to insert a log entry, it is a bug. The client code can’t fix it. Hence, we should fail in this case (aka let it crash).

With multi we need to do something like:

Multi.new()
|> Multi.insert(:user, user_changeset)
|> Multi.run(:log, fn repo, %{user: user} ->
  {:ok, repo.insert!(%LogData{user_id: user.id, ...})}
end)
|> ...

Which leads us to the weird situation where in some places we’re using Multi.insert, while in others Repo.insert!. In fact, you might end up with a combination of early-exit techniques. Some parts of the transactional flow will rely on the multi chain, while some will rely on with invoked inside Multi.run. I’ve encountered examples of such code in the wild, and I find it very confusing to read.

Another downside is context map which is threaded through the multi steps. This pattern abandons plain variables in favour of a weekly structured k-v bucket. How do we know that there’s the :user field in the bucket? Because somewhere earlier there’s a step which is tagged as :user. This is implicit, and it obfuscates things for the human readers, as well as for the machine.

For example, if I mistype the name and e.g. reference :useer, the incorrect code will still compile. If I treat this field as an integer, dialyzer will not complain. And it’s not just about the compile-time tools. For example, the context map tricks the GC, which can lead to extra memory consumption and slower GC times, because the data returned from each step remains reachable until the whole transaction is finished.

In summary, Repo.transact is IMO the simplest and the least invasive approach to make some code transactional. Put your logic inside a lambda, make sure that it returns ok or error, and you’re good to go.

Yeah, I agree that multi can be occasionally useful, but given how it is overhyped and extensively used, I personally think it should not be a part of Ecto at all, but instead provided as a separate library. That would hopefully dissuade folks from reaching for it by default. Because to me, the real question is when is multi justified over transact, since the former is more complicated than the latter :slight_smile:

Yeah it sucks. I wanted it to be short and similar to transaction :slight_smile: But ideally, Repo.transaction would behave like this when fun is passed.

Yes, you can do this with Multi.run, but it is going to add a lot of needless noise. The main issue with a transaction + fun is that it uses exception for flow control, which makes the code trickier to follow, and also leads to unwanted noise. If you have a with chain, you’ll have to add the else clause and convert the error into a rollback exception. This is basically how transact works, so you don’t have to do it repeatedly all over your codebase :wink:

The code written with transact is not only shorter but also simpler, more explicit, and clearer, because the flow is implemented with vanilla Elixir.

I don’t understand what’s confusing you here. Both Repo.transact and Multi support running multiple operations in a transactional context, and rolling back + returning on first error.

13
Post #9
gregvaughn

gregvaughn

For what little my opinion matters, I actually like transact. transaction is a noun, and logically transact is the verb form of that. I like using verbs for function names.

Where Next?

Popular in Discussions Top

bartblast
StackOverflow Survey results for 2025 have been published: Gleam - 2nd most admired language Elixir - 3rd most admired language Phoenix...
New
arcanemachine
Just wondering what the community currently thinks, prefers, and/or recommends for working with UI components. (e.g. daisyUI, MishkaChele...
New
neilberkman
Carson Katri from DockYard posted today about swift-erlang-actor-system, which enables Swift programs to join Erlang clusters as distribu...
New
ryanrasti
Hey everyone – after coming off of 3 years building with Ecto, I’ve been working non-stop on Typegres, a new query builder that strongly ...
New
ashkan117
I’m wondering how do people structure their JSON Api’s with Phoenix. Using the blogs example, let’s say I have a blogs view like the foll...
New
dogweather
I’ve been brainstorming about ways to solve the N-dimensional code organization problem, and am thinking about developing a Smalltalk-lik...
New
New
bartblast
Some great comments in the home page thread shifted toward what you’d like to see in Hologram’s future, particularly standalone mode. I’v...
New
michallepicki
Hello! To save some time when switching projects or development machines, I glued together an asdf plugin that uses Erlang precompiled b...
New
gushonorato
Hey everyone, I’ve been working with Elixir for over 5 years and I’m a big enthusiast of the language. However, when starting new projec...
New

Other popular topics Top

pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement