feat: add Ecto patterns extracted from elixir-ecto/ecto source #1
+16
-16
@@ -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
|
||||
|
||||
<!-- PATTERN_COMPLETE -->
|
||||
|
||||
Reference in New Issue
Block a user