gotx

package module
v0.0.0-...-4e871e4 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2021 License: MIT Imports: 2 Imported by: 2

README

gotx

Go transaction library inspired by Spring Framework that brings you can handle transactions without being aware of the difference in data sources such as spanner, redis or rdbmds

CI

Motivation

In the architecture of web applications, differences in data sources are absorbed by layers such as Repository and Dao. However, transactions straddle Repository and Dao.
I created this library from the desire to do simple coding by providing a Transactor that absorbs the difference in transaction behavior between data sources such as redis and spanner.

Features

  • Transaction Abstraction Layer

  • Database Sharding

  • Write-Through

Installation

go get github.com/knocknote/gotx

API

  • gotx has three core interfaces Transactor, ConnectionProvider, ClientProvider.
  • You can create any transaction behavior by implementing these interfaces.
Transactor
Method Description
Required Support a current transaction, create a new one if none exists.
RequiresNew Create a new transaction, and suspend the current transaction if one exists.
  • Each method has the following options.
Option Description
ReadOnly This option makes it a read-only transaction.
RollbackOnly This option ensures that the transaction rolls back even if it succeeds. Mainly used in test classes.
ConnectionProvider
  • A strategy to get raw connections such as *spanner.Client and *sql.DB.
  • You can create any ConnectionProvider by implementing the following method.
Method Description
CurrentConnection returns raw connections like spanner.Client or sql.DB
ClientProvider
  • Provides client classes for executing queries of rdbms or spanner .
  • You can create any ClientProvider by implementing the following method.
Method Description
CurrentClient returns api for executing query.

Usage

Here is the sample UseCase class.

  • Case 1: use read-write transaction
  • Case 2: use readonly transaction
  • Case 3: no transaction(just call repository method)
type MyUseCase struct {
  transactor gotx.Transactor
  repository Reppsitory
}

func (u *MyUseCase) Do(ctx context.Context) error {

  // Case 1
  tx1Err := u.transactor.Required(ctx, func(ctx context.Context) error {
    // do in transaction
    res, err := u.repository.FindByID(ctx, "A")
    if err != nil {
      return err
    }
    model.value += 100
    return u.repository.Update(ctx, model)
  })

  // Case 2
  var res model.Model
  tx2Err := u.transactor.Required(ctx, func(ctx context.Context) error {
    // do in readonly transaction 
    var err error
    res, err = u.repository.FindByID(ctx, "A")
    return err
  }, gotx.OptionReadOnly())

  // Case 3
  // no transaction     
  model, err := u.repository.FindByID(ctx, "A")
}
RDBMS

Here is the sample with using PostgreSQL for datasource.

import (
  "context"
  "database/sql"

  gotx "github.com/knocknote/gotx/rdbms"

  _ "github.com/lib/pq"
)

func DependencyInjection() {
  connection, _ := sql.Open("postgres", "postgres://postgres:password@localhost/testdb?sslmode=disable")
  connectionProvider := gotx.NewDefaultConnectionProvider(connection)
  clientProvider := gotx.NewDefaultClientProvider(connectionProvider)
  transactor := gotx.NewTransactor(connectionProvider)

  repository := NewRDBRepository(clientProvider)
  // MyUseCase code is independent on RDBMS transaction behavior.
  usecase := NewMyUseCase(transactor, repository)
}

type RDBRepository struct {
  clientProvider gotx.ClientProvider
}

// Repository is unaware of transactions
func (r *RDBRepository) FindByID(ctx context.Context, userID string) (*model.Model, error) {
  //Case 1 client is `sql.Tx`
  //Case 2 client is `sql.Tx(readonly)` 
  //Case 3 client is `sql.DB or interface provided by gotx.ConnectionProvider `
  client := r.clientProvider.CurrentClient(ctx)

  // use ORM like sqlboiler
  return model.FindModel(ctx, client, userID)
}
Google Cloud Spanner
  • Here is the sample with using Google Cloud Spanner for datasource.
  • Cloud Spanner does not support nested transactions. So don't use transactor.RequiresNew.
import (
  "context"

  "cloud.google.com/go/spanner"
  gotx "github.com/knocknote/gotx/spanner"
)

func DependencyInjection() {
  connection, _ := spanner.NewClient(context.Background(), "projects/local-project/instances/test-instance/databases/test-database")
  connectionProvider := gotx.NewDefaultConnectionProvider(connection)
  clientProvider := gotx.NewDefaultClientProvider(connectionProvider)
  transactor := gotx.NewTransactor(connectionProvider)
  
  repository := NewSpannerRepository(clientProvider)
  // MyUseCase code is independent on Spanner transaction behavior.
  useCase := NewMyUseCase(transactor, repository)
}

type SpannerRepository struct {
  clientProvider gotx.ClientProvider
}

// Repository is unaware of transactions
func (r *SpannerRepository) FindByID(ctx context.Context, userID string) (*model.Model, error)  {
  //Case 1 reader is `spanner.ReadWriteTransaction` 
  //Case 2 reader is `spanner.ReadOnlyTransaction` 
  //Case 3 reader is `spanner.ReadOnlyTransaction` (spanner.Client.Single())
  reader := r.clientProvider.CurrentClient(ctx).Reader(ctx)

  // use ORM like https://github.com/cloudspannerecosystem/yo
  return model.FindModel(ctx, reader, userID)
}

func (r *SpannerRepository) Update(ctx context.Context, target *model.Model) error  {
  client := r.clientProvider.CurrentClient(ctx)
  //Case 1 use `spanner.Client.BufferWrite(mutations)`
  //Case 2 returns error
  //Case 3 use `spanner.Client.Apply(ctx,mutations)`
  return clieny.ApplyOrBufferWrite(ctx,target.Update(ctx))
}
Redis

Here is the sample with using Redis for datasource.

import (
  "context"
  
  "github.com/go-redis/redis"

  gotx "github.com/knocknote/gotx/redis"
)

func DependencyInjection() {
  connection := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "",
    DB:       0,
   })
  connectionProvider := gotx.NewDefaultConnectionProvider(connection)
  clientProvider := gotx.NewDefaultClientProvider(connectionProvider)
  transactor := gotx.NewTransactor(connectionProvider)

  repository := NewRedisRepository(clientProvider)
  // MyUseCase code is independent on Redis transaction behavior.
  useCase := NewMyUseCase(transactor, repository)
}

type RedisRepository struct {
  clientProvider gotx.ClientProvider
}

// Repository is unaware of transactions
func (r *RedisRepository) FindByID(ctx context.Context, userID string) (*model.Model, error)  {
  //Case 1 reader is `redis.Client` and writer is `redis.Pipeliner` 
  //Case 2 reader is `redis.Client` and writer is `redis.Pipeliner` read only option is unsupported 
  //Case 3 reader and writer is `redis.Client`
  reader, writer := r.clientProvider.CurrentClient(ctx)

  // calling `writer.Get` returns empty result in `redis.TxPipelined` so use reader to use get item.
  val, err := reader.Get(userID).Result()
  var model &Model
  //TODO unmarshal val
  return model, err
}
Database Sharding
  • Select specified connection from []*sql.DB by the sharding key.
  • Use ShardingConnectionProvider to get the sql.DB determined by the hash slot.
import (
  "context"
  "database/sql"

  gotx "github.com/knocknote/gotx/rdbms"
)

type shardKey string

var shardKeyUser shardKey = "userID"

func DependencyInjection() {
  var userCons []*sql.DB // create sql.DB for each sharded database
  userShardKeyProvider := func(ctx context.Context) string {
    return ctx.Value(shardKeyUser).(string)
  }
  // use hash-slot
  userConnectionProvider := gotx.NewShardingConnectionProvider(userCons, 127, userShardKeyProvider)
  userTransactor := gotx.NewShardingTransactor(userConnectionProvider, userShardKeyProvider)
  userClientProvider := gotx.NewShardingDefaultClientProvider(userConnectionProvider, userShardKeyProvider)

  repository := NewSpannerRepository(userClientProvider)
  usecase := NewMyUseCase(userTransactor, repository)
}

func (u *UseCase) Do(ctx context.Context) error{
  userID := "test"
  return u.transactor.Required(context.WithValue(ctx, shardKeyUser, userID), func(ctx context.Context){
    // in target shard transaction scope 
    return u.repostiory.Update(ctx, model)
  })
}
Multiple Database Sharding
  • Select specified connection from multiple []*sql.DB by the sharding key.
  • Use CompositeTransactor to handle multiple transactions transparently with UseCase.
  • This is not a distributed transaction like XA.
import (
  "context"
  "database/sql"

  "github.com/knocknote/gotx"
  gotxrdbms "github.com/knocknote/gotx/rdbms"
)

type shardKey string

var shardKeyUser shardKey = "userID"
var shardKeyGuild shardKey = "guildID"

func DependencyInjection() {
  // user shard connections
  var userCons []*sql.DB // create sql.DB for each sharded database
  userShardKeyProvider := func(ctx context.Context) string {
    return ctx.Value(shardKeyUser).(string)
  }
  userConnectionProvider := gotxrdbms.NewShardingConnectionProvider(userCons, 127, userShardKeyProvider)
  userTransactor := gotxrdbms.NewShardingTransactor(userConnectionProvider, userShardKeyProvider)
  userClientProvider := gotxrdbms.NewShardingDefaultClientProvider(userConnectionProvider, userShardKeyProvider)

  // guild shard connections
  var guildCons []*sql.DB // create sql.DB for each sharded database
  guildShardKeyProvider := func(ctx context.Context) string {
    return ctx.Value(shardKeyGuild).(string)
  }
  guildConnectionProvider := gotxrdbms.NewShardingConnectionProvider(guildCons, 127, guildShardKeyProvider)
  guildTransactor := gotxrdbms.NewShardingTransactor(guildConnectionProvider, guildShardKeyProvider)
  guildClientProvider := gotxrdbms.NewShardingDefaultClientProvider(guildConnectionProvider, guildShardKeyProvider)

  // composite transaction
  userRepository := NewUserRepository(userClientProvider)
  guildRepository := NewGuildRepository(guildClientProvider)
  transactor := gotxrdbms.NewCompositeTransactor(userTransactor, guildTransactor)
  usecase := NewMyUseCase(transactor, userRepository,guildRepository)
}

func (u *UseCase) Do(ctx context.Context) error {
	
  childCtx := context.WithValue(context.WithValue(ctx, shardKeyUser, "userID"), shardKeyGuild, "guildID")
  
  return u.transactor.Required(childCtx, func(ctx context.Context) error {
    // in target shard transaction scope. ( one guild Tx and one user Tx begins. )
    if err := u.guildRepostiory.Update(ctx, guildModel); err != nil {
      return err
    }
    return u.userRepostiory.Update(ctx, userModel)
  }
} 
Force rollback during test

You can always roll back the test DB only for unit tests without changing the production code.

func Test_Success(t *testing.T) {

  // construct test target 
  transactor := gotx.NewTransactor(...)
  useCase := NewUserCase(transactor,repository)
 
  // execute test in rollback-only transaction
  err := transactor.Required(ctx, func(ctx context.Context) error { 
    model, err := useCase.Do(ctx)
    if err != nil {
    	return err
    }
    // assert updated data in database or return value
  }, gotx.OptionRollbackOnly()) // <- rollback only option
  
  if err != nil {
    t.Error(err)
  }
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetHashSlotRange

func GetHashSlotRange(size int, maxSlot uint32) []uint32

func GetIndexByHash

func GetIndexByHash(hashSlotRange []uint32, shardKey []byte, maxSlot uint32) int

Types

type CompositeTransactor

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

func NewCompositeTransactor

func NewCompositeTransactor(transactors ...Transactor) *CompositeTransactor

func (*CompositeTransactor) Required

func (t *CompositeTransactor) Required(ctx context.Context, fn DoInTransaction, options ...Option) error

func (*CompositeTransactor) RequiresNew

func (t *CompositeTransactor) RequiresNew(ctx context.Context, fn DoInTransaction, options ...Option) error

type Config

type Config struct {
	ReadOnly     bool
	RollbackOnly bool
	VendorOption interface{}
}

func NewDefaultConfig

func NewDefaultConfig() Config

type DoInTransaction

type DoInTransaction func(ctx context.Context) error

type Option

type Option interface {
	Apply(*Config)
}

type ReadOnly

type ReadOnly bool

read only transaction

func OptionReadOnly

func OptionReadOnly() ReadOnly

func (ReadOnly) Apply

func (o ReadOnly) Apply(c *Config)

type RollbackOnly

type RollbackOnly bool

rollback only transaction

func OptionRollbackOnly

func OptionRollbackOnly() RollbackOnly

func (RollbackOnly) Apply

func (o RollbackOnly) Apply(c *Config)

type Transactor

type Transactor interface {
	// Support a current transaction, create a new one if none exists.
	Required(ctx context.Context, fn DoInTransaction, options ...Option) error
	// Create a new transaction, and suspend the current transaction if one exists.
	RequiresNew(ctx context.Context, fn DoInTransaction, options ...Option) (err error)
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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