transactor

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2024 License: MIT Imports: 1 Imported by: 4

README

transactor

Go Reference Main Pipeline Coverage

The transactor pattern is a way to manage transactions seamlessly. You can inject your transactor in your services to make transactions completely transparently.

It relies mostly on the Transactor interface:

type Transactor interface {
  WithinTransaction(context.Context, func(context.Context) error) error
}

WithinTransaction starts a new transaction and adds it to the context. Any repository method can then retrieve a transaction from the context or fallback to the initial DB handler. The transaction is committed if the provided function doesn't return an error. It's rollbacked otherwise.

Usage

Installation
go get github.com/Thiht/transactor

The database/sql default implementation (stdlib) is included in the github.com/Thiht/transactor package. Additional implementations are available in separate modules:

  • the pgx implementation is available in github.com/Thiht/transactor/pgx,
  • the sqlx implementation is available in github.com/Thiht/transactor/sqlx.

The following examples use the stdlib implementation, but the code isn't too different with the other implementations.

Initialize a transactor

This example uses database/sql with the pgx driver, but any database/sql driver can be used.

import stdlibTransactor "github.com/Thiht/transactor/stdlib"

db, _ := sql.Open("pgx", dsn)

transactor, dbGetter := stdlibTransactor.NewTransactor(
  db,
  stdlibTransactor.NestedTransactionsSavepoints,
)

The currently available strategies for nested transactions with the stdlib implementation are:

Use the dbGetter in your repositories

Instead of injecting the *sql.DB handler directly to your repositories, you now have to inject the dbGetter. It will return the appropriate DB handler depending on whether the current execution is in a transaction.

type store struct {
-  db *sql.DB
+  dbGetter stdlibTransactor.DBGetter
}

func (s store) GetBalance(ctx context.Context, account string) (int, error) {
  var balance int
-  err := s.db.QueryRowContext(
+  err := s.dbGetter(ctx).QueryRowContext(
    ctx,
    `SELECT balance FROM accounts WHERE account = $1`,
    account,
  ).Scan(&balance)
  return balance, err
}

You can use the IsWithinTransaction helper if you need to implement different behaviours depending on whether a transaction is running. For example with PostgreSQL, you could add FOR UPDATE conditionally:

func (s store) GetBalance(ctx context.Context, account string) (int, error) {
  query := `SELECT balance FROM accounts WHERE account = $1`
  if stdlibTransactor.IsWithinTransaction(ctx) {
    query += ` FOR UPDATE`
  }

  // ...
}
Use the transactor in your services
type service struct {
  balanceStore stores.Balance
  transactor transactor.Transactor
}

func (s service) IncreaseBalance(ctx context.Context, account string, amount int) error {
  return s.transactor.WithinTransaction(ctx, func(ctx context.Context) error {
    balance, err := s.balanceStore.GetBalance(ctx, account)
    if err != nil {
      return err
    }

    balance += amount

    err = s.balanceStore.SetBalance(ctx, account, balance)
    if err != nil {
      return err
    }

    return nil
  })
}

Thanks to nested transactions support, you can even call your services within a transaction:

type service struct {
  balanceStore stores.Balance
  transactor transactor.Transactor
}

func (s service) TransferBalance(
  ctx context.Context,
  fromAccount, toAccount string,
  amount int,
) error {
  return s.transactor.WithinTransaction(ctx, func(ctx context.Context) error {
    err := s.DecreaseBalance(ctx, fromAccount, amount)
    if err != nil {
      return err
    }

    err = s.IncreaseBalance(ctx, toAccount, amount)
    if err != nil {
      return err
    }

    return nil
  })
}

func (s service) IncreaseBalance(ctx context.Context, account string, amount int) error {
  return s.transactor.WithinTransaction(ctx, func(ctx context.Context) error {
    // ...
  })
}

func (s service) DecreaseBalance(ctx context.Context, account string, amount int) error {
  return s.transactor.WithinTransaction(ctx, func(ctx context.Context) error {
    // ...
  })
}

[!WARNING] Transactions are not thread safe, so make sure not to call code making concurrent database access inside WithinTransaction

Testing

In your tests, you can inject a fake transactor and dbGetter, using NewFakeTransactor:

transactor, dbGetter := stdlibTransactor.NewFakeTransactor(db)

The fake transactor will simply execute its callback function, and the dbGetter will return the provided db handler.

This strategy works because when using this library, you don't have to worry about how transactions are made, just about returning errors appropriately in WithinTransaction.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Transactor

type Transactor interface {
	// WithinTransaction executes the given function within a transaction.
	// The transaction is added to the context, so it has to be retrieved
	// appropriately depending on the transactor implementation.
	WithinTransaction(context.Context, func(context.Context) error) error
}

Directories

Path Synopsis
pgx module
sqlx module

Jump to

Keyboard shortcuts

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