dendrite.daemon/docs/uow-refactor.md

124 lines
2.9 KiB
Markdown

# Unit of Work — Refactor Notes
## Current Issues
### 1. Lifetime
`UnitOfWork` is created once in the handler constructor and reused across every `Handle` call.
It should be **per-request**, created inside `Handle`.
### 2. Transaction leaks out
`Begin()` returns `*sql.Tx`, which is then passed manually to each repo constructor.
The UoW should own that wiring — callers should never touch the transaction directly.
### 3. Eager instantiation (rejected solution)
Having UoW create all repos in `Begin()` solves the leaking transaction, but forces every handler
to pay for repos it doesn't need. Not acceptable.
---
## Solution: Lazy Initialization
UoW exposes accessor methods that initialize each repo on first access, wired to the internal transaction.
Unused repos are never instantiated.
```go
type UnitOfWork struct {
FileStore *store.FileStore
tx *sql.Tx
tags ITagRepository
notes INoteRepository
}
func NewUnitOfWork() *UnitOfWork {
return &UnitOfWork{FileStore: store.NewFileStore()}
}
func (u *UnitOfWork) Begin() error {
tx, err := persistence.GetDBContext().DB.Begin()
if err != nil {
return err
}
u.tx = tx
return nil
}
func (u *UnitOfWork) Tags() ITagRepository {
if u.tags == nil {
u.tags = NewTagRepository(u.tx)
}
return u.tags
}
func (u *UnitOfWork) Notes() INoteRepository {
if u.notes == nil {
u.notes = NewNoteRepository(u.tx)
}
return u.notes
}
func (u *UnitOfWork) Commit() error {
if err := u.FileStore.Flush(); err != nil {
u.tx.Rollback()
u.tx = nil
return err
}
if err := u.tx.Commit(); err != nil {
u.FileStore.Rollback()
u.tx = nil
return err
}
u.tx = nil
return nil
}
func (u *UnitOfWork) Rollback() {
if u.tx == nil {
return
}
u.tx.Rollback()
u.FileStore.Rollback()
u.tx = nil
}
```
---
## Usage in a Handler
```go
func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any, error) {
var cmd createNoteCommand
if err := json.Unmarshal(raw, &cmd); err != nil {
return nil, err
}
uow := repositories.NewUnitOfWork()
if err := uow.Begin(); err != nil {
return nil, err
}
defer uow.Rollback()
// Notes() is never called — never instantiated
dbTags, err := uow.Tags().GetByNames(ctx, tags)
if err != nil {
return nil, err
}
// ...
uow.FileStore.Stage(cmd.Path, data)
return cmd.Path, uow.Commit()
}
```
---
## Properties of This Design
| Property | Result |
|---|---|
| Transaction leaks out of UoW | No — `tx` is private |
| Repos instantiated when not needed | No — lazy on first access |
| Handler controls which repos it uses | Yes — only accessed repos are created |
| UoW created per-request | Yes — inside `Handle`, not constructor |
| Handler has no persistent state | Yes — can be zero-value struct |