oniontx

package module
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2025 License: MIT Imports: 3 Imported by: 4

README

oniontx drawing

test Release GitHub go.mod Go version Go Report Card GitHub release date GitHub last commit GitHub MIT license

oniontx allows to move transferring transaction management from the Persistence (repository) layer to the Application (service) layer using owner defined contract.

drawing

🔴 NOTE: Transactor was designed to work with only the same instance of the "repository" (*sql.DB, etc.)

The key features:

stdlib package

Transactor implementation for stdlib:

// Look at to `github.com/kozmod/oniontx` to see `Transactor` implementation for standard library
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	ostdlib "github.com/kozmod/oniontx/stdlib"
)

func main() {
	var (
		db *sql.DB // database instance

		tr = ostdlib.NewTransactor(db)
		r1 = repoA{t: tr}
		r2 = repoB{t: tr}
	)

	err := tr.WithinTx(context.Background(), func(ctx context.Context) error {
		err := r1.Insert(ctx, "repoA")
		if err != nil {
			return err
		}
		err = r2.Insert(ctx, "repoB")
		if err != nil {
			return err
		}
		return nil
	})

	if err != nil {
		log.Fatal(err)
	}
	
}

type repoA struct {
	t *ostdlib.Transactor
}

func (r *repoA) Insert(ctx context.Context, val string) error {
	ex := r.t.GetExecutor(ctx)
	_, err := ex.ExecContext(ctx, `INSERT INTO tx (text) VALUES ($1)`, val)
	if err != nil {
		return fmt.Errorf("repoA.Insert: %w", err)
	}
	return nil
}

type repoB struct {
	t *ostdlib.Transactor
}

func (r *repoB) Insert(ctx context.Context, val string) error {
	ex := r.t.GetExecutor(ctx)
	_, err := ex.ExecContext(ctx, `INSERT INTO tx (text) VALUES ($1)`, val)
	if err != nil {
		return fmt.Errorf("repoB.Insert: %w", err)
	}
	return nil
}

test/integration module contains more complicated example.


Default implementation examples for libs

Examples of default implementation of Transactor (sqlx, pgx, gorm, redis, mongo):


Custom implementation

If it's required, oniontx allowed opportunity to implements custom algorithms for maintaining transactions (examples).

Interfaces:
type (
	// Mandatory
	TxBeginner[T Tx] interface {
		comparable
		BeginTx(ctx context.Context) (T, error)
	}
	
	// Mandatory
	Tx interface {
		Rollback(ctx context.Context) error
		Commit(ctx context.Context) error
	}

	// Optional - using to putting/getting transaction from `context.Context` 
	// (library contains default `СtxOperator` implementation)
	СtxOperator[T Tx] interface {
		Inject(ctx context.Context, tx T) context.Context
		Extract(ctx context.Context) (T, bool)
	}
)
Examples

This examples based on stdlib pacakge.

TxBeginner and Tx implementations:

// Prepared contracts for execution
package db

import (
	"context"
	"database/sql"

	"github.com/kozmod/oniontx"
)

// Executor represents common methods of sql.DB and sql.Tx.
type Executor interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

// DB is sql.DB wrapper, implements oniontx.TxBeginner.
type DB struct {
	*sql.DB
}

func (db *DB) BeginTx(ctx context.Context) (*Tx, error) {
	var txOptions sql.TxOptions
	for _, opt := range opts {
		opt.Apply(&txOptions)
	}
	tx, err := db.DB.BeginTx(ctx, &txOptions)
	return &Tx{Tx: tx}, err
}

// Tx is sql.Tx wrapper, implements oniontx.Tx.
type Tx struct {
	*sql.Tx
}

func (t *Tx) Rollback(_ context.Context) error {
	return t.Tx.Rollback()
}

func (t *Tx) Commit(_ context.Context) error {
	return t.Tx.Commit()
}

Repositories implementation:

package repoA

import (
	"context"
	"fmt"

	"github.com/kozmod/oniontx"

	"github.com/user/some_project/internal/db"
)

type RepositoryA struct {
	Transactor *oniontx.Transactor[*db.DB, *db.Tx]
}

func (r RepositoryA) Insert(ctx context.Context, val int) error {
	var executor db.Executor
	executor, ok  := r.Transactor.TryGetTx(ctx)
	if !ok {
		executor = r.Transactor.TxBeginner()
	}
	_, err := executor.ExecContext(ctx, "UPDATE some_A SET value = $1", val)
	if err != nil {
		return fmt.Errorf("update 'some_A': %w", err)
	}
	return nil
}
package repoB

import (
	"context"
	"fmt"
	
	"github.com/kozmod/oniontx"
	
	"github.com/user/some_project/internal/db"
)

type RepositoryB struct {
	Transactor *oniontx.Transactor[*db.DB, *db.Tx]
}

func (r RepositoryB) Insert(ctx context.Context, val int) error {
	var executor db.Executor
	executor, ok := r.Transactor.TryGetTx(ctx)
	if !ok {
		executor = r.Transactor.TxBeginner()
	}
	_, err := executor.ExecContext(ctx, "UPDATE some_A SET value = $1", val)
	if err != nil {
		return fmt.Errorf("update 'some_A': %w", err)
	}
	return nil
}

UseCase implementation:

package usecase

import (
	"context"
	"fmt"
)

type (
	// transactor is the contract of  the oniontx.Transactor
	transactor interface {
		WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)
	}

	// Repo is the contract of repositories
	repo interface {
		Insert(ctx context.Context, val int) error
	}
)

type UseCase struct {
	RepoA repo
	RepoB repo

	Transactor transactor
}

func (s *UseCase) Exec(ctx context.Context, insert int) error {
	err := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {
		if err := s.RepoA.Insert(ctx, insert); err != nil {
			return fmt.Errorf("call repository A: %w", err)
		}
		if err := s.RepoB.Insert(ctx, insert); err != nil {
			return fmt.Errorf("call repository B: %w", err)
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf(" execute: %w", err)
	}
	return nil
}

Configuring:

package main

import (
	"context"
	"database/sql"
	"os"

	oniontx "github.com/kozmod/oniontx"
	
	"github.com/user/some_project/internal/repoA"
	"github.com/user/some_project/internal/repoB"
	"github.com/user/some_project/internal/usecase"
)


func main() {
	var (
		database *sql.DB // database pointer

		wrapper    = &db.DB{DB: database}
		operator   = oniontx.NewContextOperator[*db.DB, *db.Tx](&wrapper)
		transactor = oniontx.NewTransactor[*db.DB, *db.Tx](wrapper, operator)

		repositoryA = repoA.RepositoryA{
			Transactor: transactor,
		}
		repositoryB = repoB.RepositoryB{
			Transactor: transactor,
		}

		useCase = usecase.UseCase{
			RepoA: &repositoryA,
			RepoB: &repositoryB,
			Transactor:  transactor,
		}
	)

	err := useCase.Exec(context.Background(), 1)
	if err != nil {
		os.Exit(1)
	}
}

Execution transaction in the different use cases

Execution the same transaction for different usecases with the same oniontx.Transactor instance

UseCases:

package a

import (
	"context"
	"fmt"
)

type (
	// transactor is the contract of  the oniontx.Transactor
	transactor interface {
		WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)
	}

	// Repo is the contract of repositories
	repoA interface {
		Insert(ctx context.Context, val int) error
		Delete(ctx context.Context, val float64) error
	}
)

type UseCaseA struct {
	Repo repoA

	Transactor transactor
}

func (s *UseCaseA) Exec(ctx context.Context, insert int, delete float64) error {
	err := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {
		if err := s.Repo.Insert(ctx, insert); err != nil {
			return fmt.Errorf("call repository - insert: %w", err)
		}
		if err := s.Repo.Delete(ctx, delete); err != nil {
			return fmt.Errorf("call repository - delete: %w", err)
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("usecaseA - execute: %w", err)
	}
	return nil
}
package b

import (
	"context"
	"fmt"
)

type (
	// transactor is the contract of  the oniontx.Transactor
	transactor interface {
		WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)
	}

	// Repo is the contract of repositories
	repoB interface {
		Insert(ctx context.Context, val string) error
	}

	// Repo is the contract of the useCase
	useCaseA interface {
		Exec(ctx context.Context, insert int, delete float64) error
	}
)

type UseCaseB struct {
	Repo     repoB
	UseCaseA useCaseA

	Transactor transactor
}

func (s *UseCaseB) Exec(ctx context.Context, insertA string, insertB int, delete float64) error {
	err := s.Transactor.WithinTx(ctx, func(ctx context.Context) error {
		if err := s.Repo.Insert(ctx, insertA); err != nil {
			return fmt.Errorf("call repository - insert: %w", err)
		}
		if err := s.UseCaseA.Exec(ctx, insertB, delete); err != nil {
			return fmt.Errorf("call usecaseB - exec: %w", err)
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("execute: %w", err)
	}
	return nil
}

Main:

package main

import (
	"context"
	"database/sql"
	"os"

	oniontx "github.com/kozmod/oniontx"

	"github.com/user/some_project/internal/db"
	"github.com/user/some_project/internal/repoA"
	"github.com/user/some_project/internal/repoB"
	"github.com/user/some_project/internal/usecase/a"
	"github.com/user/some_project/internal/usecase/b"
)

func main() {
	var (
		database *sql.DB // database pointer

		wrapper    = &db.DB{DB: database}
		operator   = oniontx.NewContextOperator[*db.DB, *db.Tx](&wrapper)
		transactor = oniontx.NewTransactor[*db.DB, *db.Tx](wrapper, operator)

		useCaseA = a.UseCaseA{
			Repo: repoA.RepositoryA{
				Transactor: transactor,
			},
		}

		useCaseB = b.UseCaseB{
			Repo: repoB.RepositoryB{
				Transactor: transactor,
			},
			UseCaseA: &useCaseA,
		}
	)

	err := useCaseB.Exec(context.Background(), "some_to_insert_useCase_A", 1, 1.1)
	if err != nil {
		os.Exit(1)
	}
}
Testing

test package contains useful examples for creating unit test:

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNilTxBeginner   = fmt.Errorf("tx beginner is nil")
	ErrNilTxOperator   = fmt.Errorf("tx operator is nil")
	ErrBeginTx         = fmt.Errorf("begin tx")
	ErrCommitFailed    = fmt.Errorf("commit failed")
	ErrRollbackFailed  = fmt.Errorf("rollback failed")
	ErrRollbackSuccess = fmt.Errorf("rollback tx")
)

Functions

This section is empty.

Types

type ContextOperator added in v0.1.1

type ContextOperator[K comparable, T Tx] struct {
	// contains filtered or unexported fields
}

ContextOperator inject and extract Tx from context.Context.

Default ContextOperator uses comparable key for context.Context value operation.

func NewContextOperator added in v0.1.1

func NewContextOperator[K comparable, T Tx](key K) *ContextOperator[K, T]

NewContextOperator returns new ContextOperator.

`key` uses as argument for extracting/injecting Tx.

func (*ContextOperator[K, T]) Extract added in v0.1.1

func (o *ContextOperator[K, T]) Extract(ctx context.Context) (T, bool)

Extract returns Tx extracted from context.Context.

Function calls `Value` with `key` as an argument, injected into ContextOperator.

func (*ContextOperator[K, T]) Inject added in v0.1.1

func (o *ContextOperator[K, T]) Inject(ctx context.Context, tx T) context.Context

Inject returns new context.Context contains Tx as value.

Function wraps context.WithValue.

type Transactor

type Transactor[B TxBeginner[T], T Tx] struct {
	// contains filtered or unexported fields
}

Transactor manage a transaction for single TxBeginner instance.

func NewTransactor

func NewTransactor[B TxBeginner[T], T Tx](
	beginner B,
	operator СtxOperator[T]) *Transactor[B, T]

NewTransactor returns new Transactor.

func (*Transactor[B, T]) TryGetTx added in v0.0.16

func (t *Transactor[B, T]) TryGetTx(ctx context.Context) (T, bool)

TryGetTx returns Tx and "true" from context.Context or return `false`.

func (*Transactor[B, T]) TxBeginner added in v0.1.1

func (t *Transactor[B, T]) TxBeginner() B

TxBeginner returns TxBeginner which using in Transactor.

func (*Transactor[B, T]) WithinTx added in v0.0.16

func (t *Transactor[B, T]) WithinTx(ctx context.Context, fn func(ctx context.Context) error) (err error)

WithinTx execute all queries with Tx and transaction Options. The function create new Tx or reuse Tx obtained from context.Context.

When WithinTx call recursively, the highest level function responsible for creating transaction and applying commit or rollback of a transaction.

tr := NewTransactor[...](...)

// The highest level.
// A transaction creates and finishes (commit/rollback) on this level.
err := tr.WithinTx(ctx, func(ctx context.Context) error {

	// A middle level.
	err := tr.WithinTx(ctx, func(ctx context.Context) error {
		return nil
	})

	// A middle level.
	err = tr.WithinTx(ctx, func(ctx context.Context) error {

		// The lowest level.
		err = tr.WithinTx(ctx, func(ctx context.Context) error {
			return nil
		})
		return nil
	})

	return err
})

NOTE:

+ a processed error returns to the highest level function for commit or rollback.

+ panics are transformed to errors with the same message.

+ higher level panics override lower level panic (that was transformed to an error) or an error.

Examples:

1 - oniontx.Test_Transactor_recursive_call 2 - test/integration/internal/stdlib/stdlib_test.go

type Tx added in v0.1.5

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

Tx represent transaction contract.

type TxBeginner added in v0.1.1

type TxBeginner[T Tx] interface {
	comparable
	BeginTx(ctx context.Context) (T, error)
}

TxBeginner is responsible for creating new Tx.

type СtxOperator added in v0.1.1

type СtxOperator[T Tx] interface {
	Inject(ctx context.Context, tx T) context.Context
	Extract(ctx context.Context) (T, bool)
}

СtxOperator is responsible for interaction with context.Context to store or extract Tx.

Directories

Path Synopsis
gorm module
pgx module
sqlx module
stdlib module
test module

Jump to

Keyboard shortcuts

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