tx

package module
v2.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2024 License: MIT Imports: 3 Imported by: 0

README

tx

Go Test Go Report Card Coverage Status Go Reference

go get github.com/aneshas/tx/v2

Package tx provides a simple abstraction which leverages context.Context in order to provide a transactional behavior which one could use in their use case orchestrator (eg. application service, command handler, etc...). You might think of it as closest thing in Go to @Transactional annotation in Java or the way you could scope a transaction in C#.

Many people tend to implement this pattern in one way or another (I have seen it and did it quite a few times), and more often then not, the implementations still tend to couple your use case orchestrator with your database adapters (eg. repositories) or on the other hand, rely to heavily on context.Context and end up using it as a dependency injection mechanism.

This package relies on context.Context in order to simply pass the database transaction down the stack in a safe and clean way which still does not violate the reasoning behind context package - which is to carry request scoped data across api boundaries - which is a database transaction in this case.

Drivers

Library currently supports pgx and stdlib sql out of the box although it is very easy to implement any additional ones you might need.

Example

Let's assume we have the following very common setup of an example account service which has a dependency to account repository.

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 Transactor interface in order to decorate the service with transactional behavior,
    // although you can choose how and when you use it freely
    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 the same or different repos

        return s.repo.Save(ctx, Account{
            // ...
        })
    })
}

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:

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
}

Again, you may freely choose how you implement this and whether or not you actually do use the wrapped transaction or not.

main

Then your main function would simply tie everything together like this for example:

func main() {
    var pool *pgxpool.Pool

    svc := NewAccountService(
        tx.New(pgxtxv5.NewDBFromPool(pool)),
        NewAccountRepo(pool),
    )

    _ = svc
}

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

Testing

You can use testtx.New() as a convenient test helper. It creates a test transactor which only calls f without setting any sort of transaction in the ctx and preserves any errors raised by f in .Err field.

Documentation

Overview

Package tx provides a simple transaction abstraction in order to enable decoupling / abstraction of persistence from application / domain logic while still leaving transaction control to the application service / use case coordinator (Something like @Transactional annotation in Java, without the annotation)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func From

func From[T any](ctx context.Context) (T, bool)

From returns underlying tx value from context if it can be type-casted to T Otherwise it returns default T, false. From returns underlying T from the context which in most cases should probably be pgx.Tx T will mostly be your Tx type (pgx.Tx, *sql.Tx, etc...) but is left as a generic type in order to accommodate cases where people tend to abstract the whole connection/transaction away behind an interface for example, something like Executor (see example).

Example:

type Executor interface {
	Exec(ctx context.Context, sql string, args ...interface{}) (pgconn.CommandTag, error)
	// ... other stuff
}

tx, err := tx.From[Executor](ctx, pool)

Or

tx, err := tx.From[pgx.Tx](ctx, pool)

Types

type DB

type DB interface {
	Begin(ctx context.Context) (Transaction, error)
}

DB represents an interface to a db capable of starting a transaction

type Option

type Option func(tx *TX)

func WithIgnoredErrors

func WithIgnoredErrors(errs ...error) Option

WithIgnoredErrors offers a way to provide a list of errors which will not cause the transaction to be rolled back.

The transaction will still be committed but the actual error will be returned by the WithTransaction method.

type TX

type TX struct {
	// contains filtered or unexported fields
}

TX represents sql transactor

func New

func New(db DB, opts ...Option) *TX

New constructs new transactor which will use provided db to handle the transaction

func (*TX) WithTransaction

func (t *TX) WithTransaction(ctx context.Context, f func(ctx context.Context) error) error

WithTransaction will wrap f in a sql transaction depending on the DB provider. This is mostly useful for when we want to control the transaction scope from application layer, for example application service/command handler. If f fails with an error, transactor will automatically try to roll the transaction back and report back any errors, otherwise, the implicit transaction will be committed.

type Transaction

type Transaction interface {
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
}

Transaction represents db transaction

type Transactor

type Transactor interface {
	// WithTransaction will wrap f in a sql transaction depending on the DB provider.
	// This is mostly useful for when we want to control the transaction scope from
	// application layer, for example application service/command handler.
	// If f fails with an error, transactor will automatically try to roll the transaction back and report back any errors,
	// otherwise, the implicit transaction will be committed.
	WithTransaction(ctx context.Context, f func(ctx context.Context) error) error
}

Transactor is a helper transactor interface added for brevity purposes, so you don't have to define your own See TX

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL