diff --git a/README.md b/README.md index 2b6af85..49596ae 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ func (s *AccountService) ProvisionAccount(ctx context.Context, r ProvisionAccoun You will notice that the service looks mostly the same as it would normally apart from embedding `Transactor` interface and wrapping the use case execution using `WithTransaction`, both of which say nothing of the way the mechanism is implemented (no infrastructure dependencies). +If the function wrapped via `WithTransaction` errors out or panics the transaction itself will be rolled back and if nil error is +returned the transaction will be committed. (this behavior can be changed by providing `WithIgnoredErrors(...)` option to `tx.New`) + ### Repo implementation Then, your repo might use postgres with pgx and have the following example implementation: @@ -133,4 +136,7 @@ func main() { This way, your infrastructural concerns stay in the infrastructure layer where they really belong. -*Please note that this is only one way of using the abstraction* \ No newline at end of file +*Please note that this is only one way of using the abstraction* + +## Next up +- [ ] Add a way to configure transaction isolation levels for individual drivers eg. `pgxtx.NewDBFromPool(pool, ...opts)` \ No newline at end of file diff --git a/example/example.go b/example/example.go new file mode 100644 index 0000000..649ca92 --- /dev/null +++ b/example/example.go @@ -0,0 +1,98 @@ +package example + +import ( + "context" + "github.com/aneshas/tx/v2" + "github.com/aneshas/tx/v2/pgxtxv5" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" +) + +func main() { + var pool *pgxpool.Pool + + svc := NewAccountService( + tx.New(pgxtxv5.NewDBFromPool(pool)), + NewAccountRepo(pool), + ) + + _ = svc +} + +type Account struct { + // ... +} + +type Repo interface { + Save(ctx context.Context, account Account) error + Find(ctx context.Context, id int) (*Account, error) +} + +func NewAccountService(transactor tx.Transactor, repo Repo) *AccountService { + return &AccountService{Transactor: transactor, repo: repo} +} + +type AccountService struct { + // Embedding transactional behavior in your service + tx.Transactor + + repo Repo +} + +type ProvisionAccountReq struct { + // ... +} + +func (s *AccountService) ProvisionAccount(ctx context.Context, r ProvisionAccountReq) error { + return s.WithTransaction(ctx, func(ctx context.Context) error { + // ctx contains an embedded transaction and as long as + // we pass it to our repo methods, they will be able to unwrap it and use it + + // eg. multiple calls to different repos + + return s.repo.Save(ctx, Account{ + // ... + }) + }) +} + +func NewAccountRepo(pool *pgxpool.Pool) *AccountRepo { + return &AccountRepo{ + pool: pool, + } +} + +type AccountRepo struct { + pool *pgxpool.Pool +} + +func (r *AccountRepo) Save(ctx context.Context, account Account) error { + _, err := r.conn(ctx).Exec(ctx, "...") + + return err +} + +func (r *AccountRepo) Find(ctx context.Context, id int) (*Account, error) { + rows, err := r.conn(ctx).Query(ctx, "...") + if err != nil { + return nil, err + } + + _ = rows + + return nil, nil +} + +type Conn interface { + Exec(ctx context.Context, sql string, arguments ...any) (commandTag pgconn.CommandTag, err error) + Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) +} + +func (r *AccountRepo) conn(ctx context.Context) Conn { + if tx, ok := pgxtxv5.From(ctx); ok { + return tx + } + + return r.pool +}