https://github.com/kozmod/oniontx
The library for transferring transaction management to the application layer.
https://github.com/kozmod/oniontx
clean-architecture delivery-layer go golang gorm-transaction hexagonal-architecture layer-architecture onion-architecture pgx-transaction repository sql sql-transaction sql-transactions sqlx-transaction transaction tx
Last synced: 7 months ago
JSON representation
The library for transferring transaction management to the application layer.
- Host: GitHub
- URL: https://github.com/kozmod/oniontx
- Owner: kozmod
- License: mit
- Created: 2023-05-19T19:30:42.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-08-23T05:52:26.000Z (about 1 year ago)
- Last Synced: 2024-08-23T06:51:29.689Z (about 1 year ago)
- Topics: clean-architecture, delivery-layer, go, golang, gorm-transaction, hexagonal-architecture, layer-architecture, onion-architecture, pgx-transaction, repository, sql, sql-transaction, sql-transactions, sqlx-transaction, transaction, tx
- Language: Go
- Homepage:
- Size: 712 KB
- Stars: 5
- Watchers: 2
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: Readme.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# oniontx
![]()
[](https://github.com/kozmod/oniontx/actions/workflows/test.yml)
[](https://github.com/kozmod/oniontx/actions/workflows/release.yml)

[](https://goreportcard.com/report/github.com/kozmod/oniontx)


[](https://github.com/kozmod/oniontx/blob/dev/LICENSE)`oniontx` allows to move transferring transaction management from the `Persistence` (repository) layer to the `Application` (service) layer using owner defined contract.
#![]()
đź”´ **NOTE:** `Transactor` was designed to work with only the same instance of the "repository" (`*sql.DB`, etc.)
### The key features:
- [**default implementation for `stdlib`**](#stdlib)
- [**default implementation for popular libraries**](#libs)
- [**custom implementation's contract**](#custom)
- [**simple testing with testing frameworks**](#testing)---
### `stdlib` package
`Transactor` implementation for `stdlib`:
```go
// Look at to `github.com/kozmod/oniontx` to see `Transactor` implementation for standard library
package mainimport (
"context"
"database/sql"
"fmt"
"log"
"testing"ostdlib "github.com/kozmod/oniontx/stdlib"
)func main() {
var (
db *sql.DB // database instancetr = ostdlib.NewTransactor(db)
r1 = repoA{t: tr}
r2 = repoB{t: tr}
)err := tr.WithinTxWithOpts(context.Background(), func(ctx context.Context) error {
err := r1.InsertInTx(ctx, "repoA")
if err != nil {
return err
}
err = r2.InsertInTx(ctx, "repoB")
if err != nil {
return err
}
return nil
},
oniontx.WithReadOnly(true),
)if err != nil {
log.Fatal(err)
}
}type repoA struct {
t *ostdlib.Transactor
}func (r *repoA) InsertInTx(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.InsertInTx: %w", err)
}
return nil
}type repoB struct {
t *ostdlib.Transactor
}func (r *repoB) InsertInTx(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.InsertInTx: %w", err)
}
return nil
}
```
[test/integration](https://github.com/kozmod/oniontx/tree/master/test) module contains more complicated
[example](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/stdlib).---
### Default implementation for database libs
`oniontx` has default implementation (as submodules) for maintaining transactions for database libraries:
[sqlx](https://github.com/jmoiron/sqlx),
[pgx](https://github.com/jackc/pgx),
[gorm](https://github.com/go-gorm/gorm).Examples:
- [sqlx](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/sqlx)
- [pgx](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/pgx)
- [gorm](https://github.com/kozmod/oniontx/tree/master/test/integration/internal/gorm)---
## Custom implementation
If it's required, `oniontx` allowed opportunity to implements custom algorithms for maintaining transactions (examples).#### Interfaces:
```go
type (
// Mandatory
TxBeginner[C Tx, O any] interface {
comparable
BeginTx(ctx context.Context, opts ...Option[O]) (C, error)
}
// Mandatory
Tx interface {
Rollback(ctx context.Context) error
Commit(ctx context.Context) error
}// Optional - if need to use options for transactions
Option[TxOpt any] interface {
Apply(in TxOpt)
}// Optional - using to putting/getting transaction from `context.Context`
// (library contains default `СtxOperator` implementation)
СtxOperator[C Tx] interface {
Inject(ctx context.Context, c C) context.Context
Extract(ctx context.Context) (C, bool)
}
)
```
### Examples
`❗` ️***This examples based on `stdlib` pacakge.***`TxBeginner` and `Tx` implementations:
```go
// Prepared contracts for execution
package dbimport (
"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, opts ...oniontx.Option[*sql.TxOptions]) (*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:
```go
package repoAimport (
"context"
"fmt""github.com/kozmod/oniontx"
"github.com/user/some_project/internal/db"
)type RepositoryA struct {
Transactor *oniontx.Transactor[*db.DB, *db.Tx, *sql.TxOptions]
}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
}
```
```go
package repoBimport (
"context"
"fmt"
"github.com/kozmod/oniontx"
"github.com/user/some_project/internal/db"
)type RepositoryB struct {
Transactor *oniontx.Transactor[*db.DB, *db.Tx, *sql.TxOptions]
}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:
```go
package usecaseimport (
"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 repoTransactor 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:
```go
package mainimport (
"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 pointerwrapper = &db.DB{DB: database}
operator = oniontx.NewContextOperator[*db.DB, *db.Tx](&wrapper)
transactor = oniontx.NewTransactor[*db.DB, *db.Tx, *sql.TxOptions](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 with transaction with options
`Option` implementation:
```go
package db// ... other contracts and abstraction implementations
// TxOption implements oniontx.Option.
type TxOption func(opt *sql.TxOptions)// Apply the TxOption to sql.TxOptions.
func (r TxOption) Apply(opt *sql.TxOptions) {
r(opt)
}// WithReadOnly set `ReadOnly` sql.TxOptions option.
func WithReadOnly(readonly bool) oniontx.Option[*sql.TxOptions] {
return TxOption(func(opt *sql.TxOptions) {
opt.ReadOnly = readonly
})
}// WithIsolationLevel set sql.TxOptions isolation level.
func WithIsolationLevel(level int) oniontx.Option[*sql.TxOptions] {
return TxOption(func(opt *sql.TxOptions) {
opt.Isolation = sql.IsolationLevel(level)
})
}```
UsCase:
```go
func (s *Usecase) Do(ctx context.Context) error {
err := s.Transactor.WithinTxWithOpts(ctx, func(ctx context.Context) error {
if err := s.RepositoryA.Do(ctx); err != nil {
return fmt.Errorf("call repositoryA: %+v", err)
}
if err := s.RepositoryB.Do(ctx); err != nil {
return fmt.Errorf("call repositoryB: %+v", err)
}
return nil
},
db.WithReadOnly(true),
db.WithIsolationLevel(6))
if err != nil {
return fmt.Errorf("execute: %v", err)
}
return nil
}
```
---
#### Execution transaction in the different use cases
***Execution the same transaction for different `usecases` with the same `oniontx.Transactor` instance***UseCases:
```go
package aimport (
"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 repoATransactor 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
}
```
```go
package bimport (
"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 useCaseATransactor 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:
```go
package mainimport (
"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 pointerwrapper = &db.DB{DB: database}
operator = oniontx.NewContextOperator[*db.DB, *db.Tx](&wrapper)
transactor = oniontx.NewTransactor[*db.DB, *db.Tx, *sql.TxOptions](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)
}
}
```[test](https://github.com/kozmod/oniontx/tree/master/test) package contains useful examples for creating unit test:
- [vektra/mockery **+** stretchr/testify](https://github.com/kozmod/oniontx/tree/main/test/integration/internal/mock/mockery)