Location>code7788 >text

Golang implements transactions based on a clean architecture

Popularity:842 ℃/2024-08-08 00:07:22

preamble

Hello everyone, this is Shirazawa here, this post is in the official go-kratos layout project'sclean structurebasis for elegant database transaction operations.

Video Explanation 📺 : Station B:ShirasawaTalkPublic number "Shirazawa Talk

image-20240726234405804

Study materials covered in this issue:

  • My open source Golang learning repository:/BaiZe1998/go-learning, all of the contents of this issue converge into a runnable demo, kit/transaction Path down.
  • kratos CLI tool:go install /go-kratos/kratos/cmd/kratos/v2@latest
  • The kratos microservices framework:/go-kratos/kratos
  • wire depends on the injected library:/google/wire
  • Domain-driven design thinking: this article is not much involved, with the relevant background knowledge to eat this article is better.

Before we start learning, let's catch up on the prior knowledge of Neat Architecture & Dependency Injection.

background knowledge

clean structure

kratos is a microservices framework in the Go language, github 🌟 23k./go-kratos/kratos

The project provides CLI tools that allow the user to create a new user interface via thekratos new xxxxIf you want to use the kratos-layout repository, create a new xxxx project that will use the code structure of the kratos-layout repository.

Warehouse Address:/go-kratos/kratos-layout

image-20240806235306095

A typical Go project layout generated by the kratos-layout project for users, in conjunction with the CLI tool, looks like this:

application
|____api
| |____helloworld
| | |____v1
| | |____errors
|____cmd
| |____helloworld
|____configs
|____internal
| |____conf
| |____data
| |____biz
| |____service
| |____server
|____test
|____pkg
|____go.mod
|____go.sum
|____LICENSE
|____README.md

dependency injection

🌟 Dependency injection enables the use and isolation of resources while avoiding the duplicate creation of resource objects, and is a great way to achieve theclean structureAn important part of the

The official kratos documentation mentions that it is highly recommended to try using wire for dependency injection, and the entire layout project is based on wire, which completes the construction of a neat architecture.

The service layer, which implements the methods defined by the rpc interface, enables external interaction and injects biz.

// GreeterService is a greeter service.
type GreeterService struct {
   

   uc *
}

// NewGreeterService new a greeter service.
func NewGreeterService(uc *) *GreeterService {
   return &GreeterService{uc: uc}
}

// SayHello implements .
func (s *GreeterService) SayHello(ctx , in *) (*, error) {
   g, err := (ctx, &{Hello: })
   if err != nil {
      return nil, err
   }
   return &{Message: "Hello " + }, nil
}

biz layer: define the repo interface and inject it into the data layer.

// GreeterRepo is a Greater repo.
type GreeterRepo interface {
   Save(, *Greeter) (*Greeter, error)
   Update(, *Greeter) (*Greeter, error)
   FindByID(, int64) (*Greeter, error)
   ListByHello(, string) ([]*Greeter, error)
   ListAll() ([]*Greeter, error)
}

// GreeterUsecase is a Greeter usecase.
type GreeterUsecase struct {
   repo GreeterRepo
   log  *
}

// NewGreeterUsecase new a Greeter usecase.
func NewGreeterUsecase(repo GreeterRepo, logger ) *GreeterUsecase {
	return &GreeterUsecase{repo: repo, log: (logger)}
}

// CreateGreeter creates a Greeter, and returns the new Greeter.
func (uc *GreeterUsecase) CreateGreeter(ctx , g *Greeter) (*Greeter, error) {
	(ctx).Infof("CreateGreeter: %v", )
	return (ctx, g)
}

data serves as the implementation layer for data access, implementing the upstream interface and injecting database instance resources.

type greeterRepo struct {
	data *Data
	log  *
}

// NewGreeterRepo .
func NewGreeterRepo(data *Data, logger )  {
	return &greeterRepo{
		data: data,
		log:  (logger),
	}
}

func (r *greeterRepo) Save(ctx , g *) (*, error) {
	return g, nil
}

func (r *greeterRepo) Update(ctx , g *) (*, error) {
	return g, nil
}

func (r *greeterRepo) FindByID(, int64) (*, error) {
	return nil, nil
}

func (r *greeterRepo) ListByHello(, string) ([]*, error) {
	return nil, nil
}

func (r *greeterRepo) ListAll() ([]*, error) {
	return nil, nil
}

db: injects data as the object to be manipulated.

type Data struct {
	// TODO wrapped database client
}

// NewData .
func NewData(c *, logger ) (*Data, func(), error) {
	cleanup := func() {
		(logger).Info("closing the data resources")
	}
	return &Data{}, cleanup, nil
}

Golang Elegant Transactions

intend

🌟 Item Acquisition: It is highly recommended to clone the repository and then operate it live.

git clone git@:BaiZe1998/
cd kit/transcation/helloworld

This directory is based on the go-kratos CLI tool using thekratos new helloworld generated and modified to implement transaction support.

Preparation is required to run the demo:

  1. Local database dev:root:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local
  2. Build the table:
CREATE TABLE IF NOT EXISTS greater (
    hello VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

ps: the Makefile provides the ability to use goose for database change management (goose is also an open source high 🌟 project, recommended to learn)

up:
	goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" up

down:
	goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" down

create:
	goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" create ${name} sql
  1. Start the service:go run ./cmd/helloworld/By means of the The HTTP service is configured to listen to localhost:8000 and GRPC to localhost:9000.

  2. Initiate a get request

image-20240807005017171

Core logic

helloworld The program is essentially a greeting service, due tokit/transcation/helloworld It's already a magically altered version, and to compare it with the default project, you can generate your ownhelloworld projects, in the same level of catalog, against which they are studied.

existinternal/biz/ file is what I changed, and to test the transaction, I added the biz layer to theCreateGreeter method, which calls the repo layer'sSave cap (a poem)Update two methods and both will succeed, but theUpdate method artificially throws an exception.

// CreateGreeter creates a Greeter, and returns the new Greeter.
func (uc *GreeterUsecase) CreateGreeter(ctx , g *Greeter) (*Greeter, error) {
   (ctx).Infof("CreateGreeter: %v", )
   var (
      greater *Greeter
      err error
   )
   //err = (ctx, func(ctx ) error {
   // // Update all hello because of hello + "updated",and insert a new hello
   // greater, err = (ctx, g)
   // _, err = (ctx, g)
   // return err
   //})
   greater, err = (ctx, g)
   _, err = (ctx, g)
   if err != nil {
      return nil, err
   }
   return greater, nil
}

// Update 人because of抛出异常
func (r *greeterRepo) Update(ctx , g *) (*, error) {
	result := (ctx).Model(&{}).Where("hello = ?", ).Update("hello", +"updated")
	if == 0 {
		return nil, ("greeter %s not found", )
	}
	return nil, ("custom error")
	//return g, nil
}

The repo layer turns on transactions

If you ignore what's in the comment above, because the database operations are independent for both repos.

func (r *greeterRepo) Save(ctx , g *) (*, error) {
   result := (ctx).Create(g)
   return g, 
}

func (r *greeterRepo) Update(ctx , g *) (*, error) {
   result := (ctx).Model(&{}).Where("hello = ?", ).Update("hello", +"updated")
   if  == 0 {
      return nil, ("greeter %s not found", )
   }
   return nil, ("custom error")
   //return g, nil
}

Even if an Update exception is thrown at the end, both save and update have succeeded and are not strongly correlated with each other, and one more piece of data is added to the database.

image-20240807005400189

The biz layer turns on transactions

So in order for the two methods at the repo level to share a transaction, you should start the transaction at the biz level using the db and pass the session for this transaction to the methods at the repo level.

🌟 How to pass it: using context becomes the logical solution.

following whichinternal/biz/ If you release the comments in the file and comment out the two lines that separate the use of transactions, and then re-run the project to request the interface, the transaction is rolled back due to the err thrown by the Update method, and the newxiaomingupdated Records.

// CreateGreeter creates a Greeter, and returns the new Greeter.
func (uc *GreeterUsecase) CreateGreeter(ctx , g *Greeter) (*Greeter, error) {
   (ctx).Infof("CreateGreeter: %v", )
   var (
      greater *Greeter
      err error
   )
   err = (ctx, func(ctx ) error {
      // Update all hello because of hello + "updated",and insert a new hello
      greater, err = (ctx, g)
      _, err = (ctx, g)
      return err
   })
   //greater, err = (ctx, g)
   //_, err = (ctx, g)
   if err != nil {
      return nil, err
   }
   return greater, nil
}

Core Realization

Since the Usecase instance at the biz level holds*DBClientThe repo layer also holds*DBClientand both represent the same database connection pool instance at the time of dependency injection.

existpkg/db/ hit the nail on the head for*DBClient The following two methods are provided:ExecTx() & DB()

At the biz level, this is accomplished by prioritizing the execution of theExecTx() method, creating the transaction, and encapsulating the two repo methods to be executed in the fn parameter, which is passed to the gorm instance'sTransaction() Methods to be implemented.

At the same time, inside Transcation, the fn() function is triggered, that is, the two repo operations of the aggregation, and it should be noted that at this time, the ctx carrying the contextTxKey transaction tx is passed as an argument to the fn function, so that the two downstream repos can access the transaction session at the biz level.

type contextTxKey struct{}

// ExecTx gorm Transaction
func (c *DBClient) ExecTx(ctx , fn func(ctx ) error) error {
   return (ctx).Transaction(func(tx *) error {
      ctx = (ctx, contextTxKey{}, tx)
      return fn(ctx)
   })
}

func (c *DBClient) DB(ctx ) * {
   tx, ok := (contextTxKey{}).(*)
   if ok {
      return tx
   }
   return 
}

When performing database operations at the repo level, try to pass theDB() method from the ctx to get the transaction session passed downstream and use it if it is there, or if not, use the repo layer's own held*DBClient, perform data access operations.

func (r *greeterRepo) Save(ctx , g *) (*, error) {
	result := (ctx).Create(g)
	return g, 
}

func (r *greeterRepo) Update(ctx , g *) (*, error) {
	result := (ctx).Model(&{}).Where("hello = ?", ).Update("hello", +"updated")
	if  == 0 {
		return nil, ("greeter %s not found", )
	}
	return nil, ("custom error")
	//return g, nil
}

bibliography

  • /post/

  • /pressly/goose

  • /go-kratos/kratos

  • /docs/getting-started/usage

  • /zh_CN/docs/

  • /zhanchenjin/p/