KineticEcto.RepoTransact (KineticEcto v1.1.1)
View SourceAdd Saša Jurić's Repo.transact/2
to your repo with use KineticEcto.RepoTransact
.
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.Postgres
use KineticEcto.RepoTransact
end
transact/2
is a replacement for Ecto.Repo.transaction/2
with a better developer
experience. In many cases, the use of transact/2
can provide code that is easier to
understand than an equivalent implementation using Ecto.Multi
.
As an example, a declarative user registration function might look like this example from Tom Konidas's blog post:
def register_user(params) do
Multi.new()
|> Multi.insert(:user, Accounts.new_user_changeset(params))
|> Multi.insert(:log, fn %{user: user} -> Logs.log_action(:user_registered, %{user: user}) end)
|> Multi.insert(:email_job, fn %{user: user} -> Mailer.enqueue_email_confirmation(user) end)
|> Repo.transaction()
|> case do
{:ok, %{user: user}} ->
{:ok, user}
{:error, _failed_operation, failed_value, _changes_so_far} ->
{:error, failed_value}
end
end
But this can be simplified with transact/2
.
def register_user(params) do
Repo.transact(fn ->
with {:ok, user} <- Accounts.create_user(params),
{:ok, _log} <- Logs.log_action(:user_registered, user),
{:ok, _job} <- Mailer.enqueue_email_confirmation(user) do
{:ok, user}
end
end)
end
In Saša's own words:
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 thewith
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 thetransact
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.
Summary
Functions
Runs the given function inside a transaction for the provided Ecto repo.
Functions
@spec transact(Ecto.Repo.t(), (-> result) | (module() -> result), Keyword.t()) :: result when result: :ok | {:ok, any()} | :error | {:error, any()}
Runs the given function inside a transaction for the provided Ecto repo.
This function is a wrapper around Ecto.Repo.transaction
, with the following differences:
- It accepts only a lambda of arity 0 or 1 (i.e. it doesn't work with
Ecto.Multi
). If the lambda returns
:ok | {:ok, result}
the transaction is committed.If the lambda returns
:error | {:error, reason}
the transaction is rolled back.- If the lambda returns any other kind of result, an exception is raised, and the transaction is rolled back.
- The result of
transact
is the value returned by the lambda.
This function accepts the same options as Ecto.Repo.transaction/2
.