KineticEcto.RepoTransact (KineticEcto v1.1.1)

View Source

Add 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 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.

Summary

Functions

Runs the given function inside a transaction for the provided Ecto repo.

Functions

transact(ecto_repo, fun, opts \\ [])

@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.