8000 GitHub - knocknote/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 rdbms
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.
/ gotx Public archive

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 rdbms

License

Notifications You must be signed in to change notification settings

knocknote/gotx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

83 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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)


8000
  // 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)
  }
}
{"resolvedServerColorMode":"day"}

About

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 rdbms

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

0