diff --git a/patterns/multi.md b/patterns/multi.md index 49e31ad..f9c9884 100644 --- a/patterns/multi.md +++ b/patterns/multi.md @@ -17,18 +17,18 @@ def reset(account, params) do end # Execute: -case Repo.transact(PasswordManager.reset(account, params)) do +case Repo.transaction(PasswordManager.reset(account, params)) do {:ok, %{account: account, log: log}} -> # success {:error, :account, changeset, _} -> # account step failed 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: ```elixir # 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 {:ok, account} -> case Repo.insert(Log.password_reset_changeset(account, params)) do @@ -50,7 +50,7 @@ end) **Example — before:** ```elixir def create_user_with_profile(params) do - Repo.transact(fn -> + Repo.transaction(fn -> case Repo.insert(User.changeset(params)) do {:ok, user} -> 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} -> Profile.changeset(user, params) end) - |> Repo.transact() + |> Repo.transaction() end # Caller: @@ -86,7 +86,7 @@ end **Don't use this when:** - 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 **Over-application example:** @@ -94,7 +94,7 @@ end # Overkill for a single operation Multi.new() |> Multi.insert(:user, User.changeset(params)) -|> Repo.transact() +|> Repo.transaction() ``` **Better alternative:** @@ -102,7 +102,7 @@ Multi.new() 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:** ```elixir def register_user(params) do - Repo.transact(fn -> + Repo.transaction(fn -> {:ok, user} = Repo.insert(User.changeset(params)) # This runs outside Multi — if it fails, user was already inserted case ExternalService.provision(user.id) do @@ -163,7 +163,7 @@ def register_user(params) do |> Multi.run(:provision, fn _repo, %{user: user} -> ExternalService.provision(user.id) end) - |> Repo.transact() + |> Repo.transaction() end ``` @@ -496,7 +496,7 @@ Enum.reduce(accounts, Multi.new(), fn account, multi -> end) # Error pattern-matching: -case Repo.transact(multi) do +case Repo.transaction(multi) do {:ok, results} -> Map.keys(results) # [{:account, 1}, {:account, 2}, ...] {:error, {:account, id}, changeset, _} -> "account #{id} failed" end @@ -547,7 +547,7 @@ def reset_passwords(accounts, params) do end # Caller knows exactly which account failed: -case Repo.transact(reset_passwords(accounts, params)) do +case Repo.transaction(reset_passwords(accounts, params)) do {:ok, _} -> :ok {:error, {:account, id}, changeset, _} -> 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"} {:ok, %{account: updated}} = PasswordManager.reset(account, valid_params) - |> Repo.transact() + |> Repo.transaction() # The changeset validity was the whole point, not the DB state assert updated.password != account.password @@ -630,7 +630,7 @@ end test "creates user with profile" do {:ok, %{user: user, profile: profile}} = UserRegistration.multi(valid_params()) - |> Repo.transact() + |> Repo.transaction() assert user.email == "test@example.com" assert profile.user_id == user.id @@ -685,7 +685,7 @@ test "comment changeset is valid given a post" do 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'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 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