Maybe improve Repo.transaction syntax

136 views
Skip to first unread message

Nils

unread,
Apr 11, 2016, 3:27:44 PM4/11/16
to elixir-ecto
Hello,

I just wanted to write down some ideas I had today while using Repo.transaction:
At the moment there are two ways to rollback a transaction. Both bring in some overhead, let me show you an example

def save(alice, bob) do
  try do
    Repo.transaction fn ->
      Repo.update!(%{alice | balance: alice.balance - 10})
      Repo.update!(%{bob | balance: bob.balance + 10})
    end
  rescue
    # or some other error...
    error in Ecto.InvalidChangesetError -> {:error, error.changeset}
  end
end

case save(alice, bob) do
  {:ok} -> ...
  {:error, changeset} -> ...
end

In this case I use try/rescue to convert the error into a return type. So that I can use pattern matching like it is done with most elixir apis. But I think try/rescue is quite uncommon so I was looking for another solution.

Another way to rollback the transaction is Repo.rollback and with some elixir 1.3 magic it can look like:
def save(alice, bob) do
  Repo.transaction fn ->
    with {:ok, _} <- Repo.update(%{alice | balance: alice.balance - 10}),
           {:ok, _} <- Repo.update(%{bob | balance: bob.balance + 10}) do 
           
      {:ok}
    else
      {:error, error} -> Repo.rollback(error)
    end
  end
end

Is there another way to do this? I found it quite suprising that Repo.transaction is throwing an exception. I think a better name for it would be Repo.transaction! to be in line with other apis.
So we could have Repo.transaction! throwing the errors from Repo.update! and Repo.rollback. And use Repo.transaction to always return {:ok, _} or {:error, _}

def save(alice, bob) do
  Repo.transaction fn ->
    # Repo.transaction will catch the errors
    Repo.update!(%{alice | balance: alice.balance - 10})
    Repo.update!(%{bob | balance: bob.balance + 10})
  end
end

{:ok, _} = save(alice, bob)

# and
try do
   Repo.transaction! fn ->
     Repo.update!(%{alice | balance: alice.balance - 10})
     Repo.update!(%{bob | balance: bob.balance + 10})
   end
 rescue
   error in Ecto.InvalidChangesetError -> {:error, error.changeset}
 end

This might also be confusing when the Repo.update! errors are not propagated further but catched by Repo.transaction...
Just some ideas, what do you think?

Gio Torres

unread,
Jun 1, 2021, 5:52:35 PM6/1/21
to elixir-ecto
Plus one on this!

I faced the same issue today.
Mostly, I use Ecto.Multi to do transactions, and it has been very satisfactory to me.

Today I had to encapsulate some code on a Repo.transaction/2 without breaking the interface, which was {:ok, result} | {:error, changeset}.

Naively, I did:

Repo.transaction fn ->
    with {:ok, alice} <- Person.add_amount(alice, -10),
           {:ok, bob} <- Person.add_amount(bob, +10) do 
      Ledger.update_balance([alice, bob])
    end
end

where
- Person.add_amount/2 :: {:ok, result} | {:error, reason}
- Ledger.update_balance/2 :: {:ok, result} | {:error, reason}

I was hoping that on Repo.transaction/2, when it receives a tuple {:error, reason}, it would rollback the transaction and return {:error, reason}, but instead it returned {:ok, {:error, reason}}, then I realized I'd have to do like what Nils suggested above:

Repo.transaction fn ->
    with {:ok, alice} <- Person.add_amount(alice, -10),
           {:ok, bob} <- Person.add_amount(bob, +10) do 
      Ledger.update_balance([alice, bob])
    else
      {:error, error} -> Repo.rollback(error)
    end
end

So, I'd suggest that Repo.transaction/2 could consider when the given function returns the conventioned tuples {:ok, result} | {:error, reason} to really bubble up these and commit or rollback based on them. :)

José Valim

unread,
Jun 1, 2021, 6:01:08 PM6/1/21
to elixi...@googlegroups.com
Unfortunately it is impossible to make this change without breaking backwards compatibility. Although you should be able to build a helper on your own project on top of the current API.

--
You received this message because you are subscribed to the Google Groups "elixir-ecto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-ecto...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-ecto/cf30196a-fa60-4303-bb94-9fb1482b49b7n%40googlegroups.com.

Thomas J.

unread,
Jun 10, 2021, 6:06:10 AM6/10/21
to elixir-ecto
Does this wrapping code make transaction behave like you'd expect to?
José, even with a new *major* Ecto version it's not possible to change the API ? Instead of raising it would return an error tuple and that will mostly fail the code anyway. Major versions include incompatible API changes.

```elixir
def transaction(fun_or_multi, opts \\ []) do
  try do
    Repo.transaction(fun_or_multi, opts)
  rescue
    error in Ecto.InvalidChangesetError -> {:error, error.changeset}
  end
end

def transaction!(fun_or_multi, opts \\ []) do
  Repo.transaction(fun_or_multi, opts)
end
```
Reply all
Reply to author
Forward
0 new messages