Tx (tx v0.4.1)
A simple composable transaction library that serves as an experimental alternative solution to Ecto.Multi.
This library intend to tackle the main problem with Ecto.Multi:
Ecto.Multi names are global, which requires the caller of a multi-function to know about the names used in the multi.
Composing complex Ecto.Multi can run into name collision if not being careful enough.
Link to this section Summary
Types
You can put a value of any Tx type on the right of <-
notation in Tx.Macro.
Functions
Compose two transactions. Run the second one only if the first one succeeds.
Combine a list of transactions [Tx.t(a)]
into a single Tx.t([a])
.
Combine two transactions Tx.t(a)
and Tx.t(b)
into a single Tx.t({a, b})
.
Avoid a transaction to rollback on exception.
Make an transaction rollback on error.
Execute the transaction Tx.t(a)
, producing an {:ok, a}
or {:error, any}
.
Make a Tx optional so if it fails, it acts as a Tx that returns {:ok, nil}.
Map over the successful result of a transaction.
Create a transaction.
Create a Tx value that always returns error.
Compose two transactions. If the first transaction fails, fallback to the second transaction.
Create a transaction that returns a pure value.
Rollback the current transaction.
The raising version of run/2
.
Run a transaction to get its result.
Convert a Tx.t(a)
into a Multi.t()
.
Link to this section Types
error_t()
@type error_t() :: any()
fn_t(a)
@type fn_t(a) :: (Ecto.Repo.t() -> {:ok, a} | {:error, error_t()})
t(a)
@type t(a) :: fn_t(a) | Ecto.Multi.t() | {:ok, a} | {:error, error_t()}
You can put a value of any Tx type on the right of <-
notation in Tx.Macro.
A Tx type is any of the following:
A Tx function :: any function that takes in a repo and returns a
{:ok, a} | {:error, error_t()}
pair. You can create a Tx function via thetx
macro, or any tx combinators likeTx.new
,Tx.pure
,Tx.concat
.An Ecto.Multi.t() :: equivalent a tx function that returns
{:ok, %{multi_name => value}}
or{:error, multi_error}
A plain
{:ok, a}
:: acts as afn_t(a)
that constantly return{:ok, a}
A plain
{:error, error_t}
:: acts as afn_t(a)
that constantly return{:ok, a}
Link to this section Functions
and_then(t, f)
Compose two transactions. Run the second one only if the first one succeeds.
This is the "bind" operation if you're familiar with Monad.
Example:
iex> Tx.pure(1) |> Tx.and_then(&{:error, &1}) |> Tx.execute(Repo)
{:error, 1}
concat(list)
Combine a list of transactions [Tx.t(a)]
into a single Tx.t([a])
.
iex> Tx.concat([Tx.pure(1), Tx.pure(2)]) |> Tx.execute(Repo)
{:ok, [1, 2]}
concat(a, b)
Combine two transactions Tx.t(a)
and Tx.t(b)
into a single Tx.t({a, b})
.
iex> Tx.concat(Tx.pure(1), Tx.pure(2)) |> Tx.execute(Repo)
{:ok, {1, 2}}
disable_rollback_on_exception(tx)
Avoid a transaction to rollback on exception.
Wrap around tx
with rollback_on_exception(tx, false)
if
you avoid tx
to rollback on exception. When an exception occurs,
you will get an {:error, exception}
instead.
enable_rollback_on_error(trans)
Make an transaction rollback on error.
You can use this function to fine-tune the rollback behaviour on specific sub-transactions.
execute(tx, repo, opts \\ [])
@spec execute(t(a()), Ecto.Repo.t(), keyword()) :: {:ok, a()} | {:error, any()}
Execute the transaction Tx.t(a)
, producing an {:ok, a}
or {:error, any}
.
Options:
rollback_on_error (Default: true): rollback transaction if the final result is an {:error, error_t()}
rollback_on_exception (Default: true): rollback transaction if an uncaught exception arises within the transaction.
Any options Ecto.Repo.transaction/2 accepts.
make_optional(tx)
Make a Tx optional so if it fails, it acts as a Tx that returns {:ok, nil}.
This operation corresponds to the "optional" combinator for the Alternative instance.
Example:
iex> Tx.new_error(1) |> Tx.execute(Repo)
{:error, 1}
iex> Tx.new_error(1) |> Tx.make_optional() |> Tx.execute(Repo)
{:ok, nil}
map(t, f)
Map over the successful result of a transaction.
Example:
iex> Tx.pure(1) |> Tx.map(&(&1 + 1)) |> Tx.execute(Repo)
{:ok, 2}
new(f)
Create a transaction.
This function creates a Tx.t() from a function of type
(Ecto.Repo.t() -> {:ok, a} | {:error, error_t()})
.
Internally, the new/1
constructor simply returns the function as
is.
Example:
iex> t = fn _repo -> {:ok, 42} end
iex> Tx.execute(Tx.new(t), Repo)
{:ok, 42}
new_error(e)
Create a Tx value that always returns error.
This operation corresponds to the "mzero"/"empty" axiom in the MonadPlus/Alternative instance.
Example:
iex> Tx.new_error(1) |> Tx.execute(Repo)
{:error, 1}
iex> Tx.new_error(1) |> Tx.or_else(fn -> {:ok, 2} end)|> Tx.execute(Repo)
{:ok, 2}
or_else(t, f)
Compose two transactions. If the first transaction fails, fallback to the second transaction.
This is the "mplus"/"<|>" operation if you're familiar with MonadPlus or Alternative.
Example:
iex> Tx.pure(1)
...> |> Tx.and_then(&{:error, &1})
...> |> Tx.or_else(&({:ok, &1}))
...> |> Tx.execute(Repo)
{:ok, 1}
pure(a)
@spec pure(a()) :: t(a())
Create a transaction that returns a pure value.
This is the "pure"/"return" operation if you're familiar with Monad.
Executing Tx with a pure value always returns the value.
pure(a)
is equivalent to new(fn _ -> {:ok, a} end)
.
iex> Tx.execute(Tx.pure(42), Repo)
{:ok, 42}
rollback(repo, error)
@spec rollback(Ecto.Repo.t(), error_t()) :: no_return()
Rollback the current transaction.
Example:
iex> Tx.new(fn repo ->
...> if 1 == 1 do
...> Tx.rollback(repo, "One cannot be equal to one")
...> else
...> {:ok, :fine}
...> end
...> end)
...> |> Tx.execute(Repo)
{:error, "One cannot be equal to one"}
run!(repo, tx)
@spec run!(Ecto.Repo.t(), t(a())) :: a()
The raising version of run/2
.
This function should be rarely needed if you use the Tx.Macro.tx
macro.
run(repo, multi)
@spec run(Ecto.Repo.t(), t(a()) | any()) :: {:ok, a()} | {:error, error_t()}
Run a transaction to get its result.
This is a generic adapter for extracting a {:ok, a} | {:error, error_t()}
from
a transaction when given a repo.
- For normal transaction
tx
as a function (default), it simply call tx.(repo) - For Ecto.Multi, it creates a sub-transaction to execute it
- For a non-transactional value, it simply returns the value
This function should be rarely needed if you use the Tx.Macro.tx
macro. You can simply use a <- t
syntax instead of a = Tx.run(repo, t)
within the tx
macro.
This function is meant to be used within a tx block or inside a Tx
closure. If you want to get a value out of a Tx, you may want to
call execute/2
instead.
to_multi(tx, name)
@spec to_multi(t(a()), term()) :: Ecto.Multi.t()
Convert a Tx.t(a)
into a Multi.t()
.
You can refer to the transactin's result ({:ok, a} | {:error, any}
) by name
.
EctoRepo.transaction(to_multi(pure(42), :foo)) => {:ok, %{foo: {:ok, 42}}}