fix: use Repo.transaction instead of non-existent Repo.transact

This commit is contained in:
2026-05-01 20:41:06 -07:00
parent 7b38ac9b2a
commit d28b9c8844
+16 -16
View File
@@ -17,18 +17,18 @@ def reset(account, params) do
end end
# Execute: # Execute:
case Repo.transact(PasswordManager.reset(account, params)) do case Repo.transaction(PasswordManager.reset(account, params)) do
{:ok, %{account: account, log: log}} -> # success {:ok, %{account: account, log: log}} -> # success
{:error, :account, changeset, _} -> # account step failed {:error, :account, changeset, _} -> # account step failed
end end
``` ```
**Why:** Each operation is named. On success, `Repo.transact` returns `{:ok, results_map}` where each key is the name given to that operation. On failure, it returns `{:error, failed_name, failed_value, changes_so_far}`, making it immediately clear which step aborted the transaction and why. This is more precise than a bare transaction function where you'd have to inspect the return value to guess which step failed. **Why:** Each operation is named. On success, `Repo.transaction` returns `{:ok, results_map}` where each key is the name given to that operation. On failure, it returns `{:error, failed_name, failed_value, changes_so_far}`, making it immediately clear which step aborted the transaction and why. This is more precise than a bare transaction function where you'd have to inspect the return value to guess which step failed.
**Anti-pattern:** Using an anonymous function with bare `case` statements inside a transaction, where failure attribution is implicit: **Anti-pattern:** Using an anonymous function with bare `case` statements inside a transaction, where failure attribution is implicit:
```elixir ```elixir
# BAD — no way to know which operation failed from the return value alone # BAD — no way to know which operation failed from the return value alone
Repo.transact(fn -> Repo.transaction(fn ->
case Repo.update(Account.password_reset_changeset(account, params)) do case Repo.update(Account.password_reset_changeset(account, params)) do
{:ok, account} -> {:ok, account} ->
case Repo.insert(Log.password_reset_changeset(account, params)) do case Repo.insert(Log.password_reset_changeset(account, params)) do
@@ -50,7 +50,7 @@ end)
**Example — before:** **Example — before:**
```elixir ```elixir
def create_user_with_profile(params) do def create_user_with_profile(params) do
Repo.transact(fn -> Repo.transaction(fn ->
case Repo.insert(User.changeset(params)) do case Repo.insert(User.changeset(params)) do
{:ok, user} -> {:ok, user} ->
case Repo.insert(Profile.changeset(user, params)) do case Repo.insert(Profile.changeset(user, params)) do
@@ -71,7 +71,7 @@ def create_user_with_profile(params) do
|> Multi.insert(:profile, fn %{user: user} -> |> Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(user, params) Profile.changeset(user, params)
end) end)
|> Repo.transact() |> Repo.transaction()
end end
# Caller: # Caller:
@@ -86,7 +86,7 @@ end
**Don't use this when:** **Don't use this when:**
- You have a single database operation (just call `Repo.insert/update/delete` directly) - You have a single database operation (just call `Repo.insert/update/delete` directly)
- Operations are simple and sequential with no branching (a plain `Repo.transact(fn -> ... end)` is more readable) - Operations are simple and sequential with no branching (a plain `Repo.transaction(fn -> ... end)` is more readable)
- The overhead of building a Multi struct is not justified by the number of operations - The overhead of building a Multi struct is not justified by the number of operations
**Over-application example:** **Over-application example:**
@@ -94,7 +94,7 @@ end
# Overkill for a single operation # Overkill for a single operation
Multi.new() Multi.new()
|> Multi.insert(:user, User.changeset(params)) |> Multi.insert(:user, User.changeset(params))
|> Repo.transact() |> Repo.transaction()
``` ```
**Better alternative:** **Better alternative:**
@@ -102,7 +102,7 @@ Multi.new()
Repo.insert(User.changeset(params)) Repo.insert(User.changeset(params))
``` ```
**Why:** `Ecto.Multi` introduces indirection. For simple cases, calling Repo functions directly or using `Repo.transact(fn -> ... end)` is clearer. The named-pipeline form pays off when 3+ operations are involved and failure attribution matters. **Why:** `Ecto.Multi` introduces indirection. For simple cases, calling Repo functions directly or using `Repo.transaction(fn -> ... end)` is clearer. The named-pipeline form pays off when 3+ operations are involved and failure attribution matters.
--- ---
@@ -144,7 +144,7 @@ end)
**Example — before:** **Example — before:**
```elixir ```elixir
def register_user(params) do def register_user(params) do
Repo.transact(fn -> Repo.transaction(fn ->
{:ok, user} = Repo.insert(User.changeset(params)) {:ok, user} = Repo.insert(User.changeset(params))
# This runs outside Multi — if it fails, user was already inserted # This runs outside Multi — if it fails, user was already inserted
case ExternalService.provision(user.id) do case ExternalService.provision(user.id) do
@@ -163,7 +163,7 @@ def register_user(params) do
|> Multi.run(:provision, fn _repo, %{user: user} -> |> Multi.run(:provision, fn _repo, %{user: user} ->
ExternalService.provision(user.id) ExternalService.provision(user.id)
end) end)
|> Repo.transact() |> Repo.transaction()
end end
``` ```
@@ -496,7 +496,7 @@ Enum.reduce(accounts, Multi.new(), fn account, multi ->
end) end)
# Error pattern-matching: # Error pattern-matching:
case Repo.transact(multi) do case Repo.transaction(multi) do
{:ok, results} -> Map.keys(results) # [{:account, 1}, {:account, 2}, ...] {:ok, results} -> Map.keys(results) # [{:account, 1}, {:account, 2}, ...]
{:error, {:account, id}, changeset, _} -> "account #{id} failed" {:error, {:account, id}, changeset, _} -> "account #{id} failed"
end end
@@ -547,7 +547,7 @@ def reset_passwords(accounts, params) do
end end
# Caller knows exactly which account failed: # Caller knows exactly which account failed:
case Repo.transact(reset_passwords(accounts, params)) do case Repo.transaction(reset_passwords(accounts, params)) do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, {:account, id}, changeset, _} -> {:error, {:account, id}, changeset, _} ->
Logger.error("Failed to reset account #{id}: #{inspect(changeset.errors)}") Logger.error("Failed to reset account #{id}: #{inspect(changeset.errors)}")
@@ -609,7 +609,7 @@ test "password reset changeset is valid" do
account = %Account{password: "letmein"} account = %Account{password: "letmein"}
{:ok, %{account: updated}} = {:ok, %{account: updated}} =
PasswordManager.reset(account, valid_params) PasswordManager.reset(account, valid_params)
|> Repo.transact() |> Repo.transaction()
# The changeset validity was the whole point, not the DB state # The changeset validity was the whole point, not the DB state
assert updated.password != account.password assert updated.password != account.password
@@ -630,7 +630,7 @@ end
test "creates user with profile" do test "creates user with profile" do
{:ok, %{user: user, profile: profile}} = {:ok, %{user: user, profile: profile}} =
UserRegistration.multi(valid_params()) UserRegistration.multi(valid_params())
|> Repo.transact() |> Repo.transaction()
assert user.email == "test@example.com" assert user.email == "test@example.com"
assert profile.user_id == user.id assert profile.user_id == user.id
@@ -685,7 +685,7 @@ test "comment changeset is valid given a post" do
end end
``` ```
**Why:** `Multi.to_list/1` reflects the state of the pipeline at build time. Deferred values (function variants, `Multi.run` callbacks, `Multi.merge` functions) are not evaluated until `Repo.transact` runs them. Test those deferred pieces independently rather than trying to inspect them through `to_list`. **Why:** `Multi.to_list/1` reflects the state of the pipeline at build time. Deferred values (function variants, `Multi.run` callbacks, `Multi.merge` functions) are not evaluated until `Repo.transaction` runs them. Test those deferred pieces independently rather than trying to inspect them through `to_list`.
--- ---
@@ -697,6 +697,6 @@ end
- If you have reusable Multi fragments to combine → `Multi.append/2` or `Multi.prepend/2` - If you have reusable Multi fragments to combine → `Multi.append/2` or `Multi.prepend/2`
- If you're updating a dynamic collection → tuple keys `{:operation, id}` - If you're updating a dynamic collection → tuple keys `{:operation, id}`
- If you want to validate changesets without hitting the DB → `Multi.to_list/1` in tests - If you want to validate changesets without hitting the DB → `Multi.to_list/1` in tests
- If operations are simple and static (no dynamic branching) → consider `Repo.transact(fn -> ... end)` instead - If operations are simple and static (no dynamic branching) → consider `Repo.transaction(fn -> ... end)` instead
<!-- PATTERN_COMPLETE --> <!-- PATTERN_COMPLETE -->