<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Golang在整潔架構基礎上實現事務

      前言

      大家好,這里是白澤,這篇文章在 go-kratos 官方的 layout 項目的整潔架構基礎上,實現優雅的數據庫事務操作。

      視頻講解 ??:B站:白澤talk,公眾號【白澤talk】

      image-20240726234405804

      本期涉及的學習資料:

      在開始學習之前,先補齊一下整潔架構 & 依賴注入的前置知識。

      預備知識

      整潔架構

      kratos 是 Go 語言的一個微服務框架,github ?? 23k,https://github.com/go-kratos/kratos

      該項目提供了 CLI 工具,允許用戶通過 kratos new xxxx,新建一個 xxxx 項目,這個項目將使用 kratos-layout 倉庫的代碼結構。

      倉庫地址:https://github.com/go-kratos/kratos-layout

      image-20240806235306095

      kratos-layout 項目為用戶提供的,配合 CLI 工具生成的一個典型的 Go 項目布局看起來像這樣:

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

      依賴注入

      ?? 通過依賴注入,實現了資源的使用和隔離,同時避免了重復創建資源對象,是實現整潔架構的重要一環。

      kratos 的官方文檔中提到,十分建議用戶嘗試使用 wire 進行依賴注入,整個 layout 項目,也是基于 wire,完成了整潔架構的搭建。

      service 層,實現 rpc 接口定義的方法,實現對外交互,注入了 biz。

      // GreeterService is a greeter service.
      type GreeterService struct {
         v1.UnimplementedGreeterServer
      
         uc *biz.GreeterUsecase
      }
      
      // NewGreeterService new a greeter service.
      func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {
         return &GreeterService{uc: uc}
      }
      
      // SayHello implements helloworld.GreeterServer.
      func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {
         g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})
         if err != nil {
            return nil, err
         }
         return &v1.HelloReply{Message: "Hello " + g.Hello}, nil
      }
      

      biz 層:定義 repo 接口,注入 data 層。

      // GreeterRepo is a Greater repo.
      type GreeterRepo interface {
         Save(context.Context, *Greeter) (*Greeter, error)
         Update(context.Context, *Greeter) (*Greeter, error)
         FindByID(context.Context, int64) (*Greeter, error)
         ListByHello(context.Context, string) ([]*Greeter, error)
         ListAll(context.Context) ([]*Greeter, error)
      }
      
      // GreeterUsecase is a Greeter usecase.
      type GreeterUsecase struct {
         repo GreeterRepo
         log  *log.Helper
      }
      
      // NewGreeterUsecase new a Greeter usecase.
      func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {
      	return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
      }
      
      // CreateGreeter creates a Greeter, and returns the new Greeter.
      func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
      	uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
      	return uc.repo.Save(ctx, g)
      }
      

      data 作為數據訪問的實現層,實現了上游接口,注入了數據庫實例資源。

      type greeterRepo struct {
      	data *Data
      	log  *log.Helper
      }
      
      // NewGreeterRepo .
      func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {
      	return &greeterRepo{
      		data: data,
      		log:  log.NewHelper(logger),
      	}
      }
      
      func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
      	return g, nil
      }
      
      func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
      	return g, nil
      }
      
      func (r *greeterRepo) FindByID(context.Context, int64) (*biz.Greeter, error) {
      	return nil, nil
      }
      
      func (r *greeterRepo) ListByHello(context.Context, string) ([]*biz.Greeter, error) {
      	return nil, nil
      }
      
      func (r *greeterRepo) ListAll(context.Context) ([]*biz.Greeter, error) {
      	return nil, nil
      }
      
      

      db:注入 data,作為被操作的對象。

      type Data struct {
      	// TODO wrapped database client
      }
      
      // NewData .
      func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
      	cleanup := func() {
      		log.NewHelper(logger).Info("closing the data resources")
      	}
      	return &Data{}, cleanup, nil
      }
      

      Golang 優雅事務

      準備

      ?? 項目獲?。簭娏医ㄗh克隆倉庫后實機操作。

      git clone git@github.com:BaiZe1998/go-learning.git
      cd kit/transcation/helloworld
      

      這個目錄基于 go-kratos CLI 工具使用 kratos new helloworld 生成,并在此基礎上修改,實現了事務支持。

      運行 demo 需要準備:

      1. 本地數據庫 dev:root:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local
      2. 建立表:
      CREATE TABLE IF NOT EXISTS greater (
          hello VARCHAR(20) NOT NULL
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
      

      ps:Makefile 中提供了使用 goose 進行數據庫變更管理的能力(goose 也是一個開源的高 ?? 項目,推薦學習)

      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. 啟動服務:go run ./cmd/helloworld/,通過 config.yaml 配置了 HTTP 服務監聽 localhost:8000,GRPC 則是 localhost:9000。

      2. 發起一個 get 請求

      image-20240807005017171

      核心邏輯

      helloworld 項目本質是一個打招呼服務,由于 kit/transcation/helloworld 已經是魔改后的版本,為了與默認項目做對比,你可以自行生成一個 helloworld 項目,在同級目錄下,對照學習。

      internal/biz/greeter.go 文件中,是我更改的內容,為了測試事務,我在 biz 層的 CreateGreeter 方法中,調用了 repo 層的 SaveUpdate 兩個方法,且這兩個方法都會成功,但是 Update 方法人為拋出一個異常。

      // CreateGreeter creates a Greeter, and returns the new Greeter.
      func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
         uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
         var (
            greater *Greeter
            err     error
         )
         //err = uc.db.ExecTx(ctx, func(ctx context.Context) error {
         // // 更新所有 hello 為 hello + "updated",且插入新的 hello
         // greater, err = uc.repo.Save(ctx, g)
         // _, err = uc.repo.Update(ctx, g)
         // return err
         //})
         greater, err = uc.repo.Save(ctx, g)
         _, err = uc.repo.Update(ctx, g)
         if err != nil {
            return nil, err
         }
         return greater, nil
      }
      
      // Update 人為拋出異常
      func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
      	result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated")
      	if result.RowsAffected == 0 {
      		return nil, fmt.Errorf("greeter %s not found", g.Hello)
      	}
      	return nil, fmt.Errorf("custom error")
      	//return g, nil
      }
      

      repo 層開啟事務

      如果忽略上文注釋中的內容,因為兩個 repo 的數據庫操作都是獨立的。

      func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
         result := r.data.db.DB(ctx).Create(g)
         return g, result.Error
      }
      
      func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
         result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated")
         if result.RowsAffected == 0 {
            return nil, fmt.Errorf("greeter %s not found", g.Hello)
         }
         return nil, fmt.Errorf("custom error")
         //return g, nil
      }
      

      即使最后拋出 Update 的異常,但是 save 和 update 都已經成功了,且彼此不強關聯,數據庫中會多增加一條數據。

      image-20240807005400189

      biz 層開啟事務

      因此為了 repo 層的兩個方法能夠共用一個事務,應該在 biz 層就使用 db 開啟事務,且將這個事務的會話傳遞給 repo 層的方法。

      ?? 如何傳遞:使用 context 便成了順理成章的方案。

      接下來將 internal/biz/greeter.go 文件中注釋的部分釋放,且注釋掉分開使用事務的兩行,此時重新運行項目請求接口,則由于 Update 方法拋出 err,導致事務回滾,未出現新增的 xiaomingupdated 記錄。

      // CreateGreeter creates a Greeter, and returns the new Greeter.
      func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
         uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
         var (
            greater *Greeter
            err     error
         )
         err = uc.db.ExecTx(ctx, func(ctx context.Context) error {
            // 更新所有 hello 為 hello + "updated",且插入新的 hello
            greater, err = uc.repo.Save(ctx, g)
            _, err = uc.repo.Update(ctx, g)
            return err
         })
         //greater, err = uc.repo.Save(ctx, g)
         //_, err = uc.repo.Update(ctx, g)
         if err != nil {
            return nil, err
         }
         return greater, nil
      }
      

      核心實現

      由于 biz 層的 Usecase 實例持有 *DBClient,repo 層也持有 *DBClient,且二者在依賴注入的時候,代表同一個數據庫連接池實例。

      pkg/db/db.go 中,為 *DBClient 提供了如下兩個方法: ExecTx() & DB()。

      在 biz 層,通過優先執行 ExecTx() 方法,創建事務,以及將待執行的兩個 repo 方法封裝在 fn 參數中,傳遞給 gorm 實例的 Transaction() 方法待執行。

      同時在 Transcation 內部,觸發 fn() 函數,也就是聚合的兩個 repo 操作,需要注意的是,此時將攜帶 contextTxKey 事務 tx 的 ctx 作為參數傳遞給了 fn 函數,因此下游的兩個 repo 可以獲取到 biz 層的事務會話。

      type contextTxKey struct{}
      
      // ExecTx gorm Transaction
      func (c *DBClient) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error {
         return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
            ctx = context.WithValue(ctx, contextTxKey{}, tx)
            return fn(ctx)
         })
      }
      
      func (c *DBClient) DB(ctx context.Context) *gorm.DB {
         tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
         if ok {
            return tx
         }
         return c.db
      }
      

      在 repo 層執行數據庫操作的時候,嘗試通過 DB() 方法,從 ctx 中獲取到上游傳遞下來的事務會話,如果有則使用,如果沒有,則使用 repo 層自己持有的 *DBClient,進行數據訪問操作。

      func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
      	result := r.data.db.DB(ctx).Create(g)
      	return g, result.Error
      }
      
      func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) {
      	result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated")
      	if result.RowsAffected == 0 {
      		return nil, fmt.Errorf("greeter %s not found", g.Hello)
      	}
      	return nil, fmt.Errorf("custom error")
      	//return g, nil
      }
      

      參考文獻

      posted on 2024-08-07 23:59  白澤talk  閱讀(752)  評論(0)    收藏  舉報

      主站蜘蛛池模板: 久久精品国产88精品久久| 无码伊人久久大杳蕉中文无码| 免费又爽又大又高潮视频| 少妇被无套内谢免费看| 亚洲熟女少妇乱色一区二区| 国产女人被狂躁到高潮小说| 中文字幕午夜福利片午夜福利片97| 国产中年熟女高潮大集合| 亚洲AV无码午夜嘿嘿嘿| 武胜县| 国产一区二区三区高清视频| 国产白嫩护士在线播放| 欧美乱妇高清无乱码免费| 亚洲青青草视频在线播放| 被喂春药蹂躏的欲仙欲死视频| 日韩有码中文字幕国产| 91九色国产成人久久精品| 成人无码精品1区2区3区免费看| 免费无码久久成人网站入口| 亚洲欧美自偷自拍视频图片| 磐安县| 国产精品成人网址在线观看 | 黄色不卡视频一区二区三区| 国产太嫩了在线观看| 日本极品少妇videossexhd| 九九热在线视频只有精品| 扒开双腿疯狂进出爽爽爽| 视频一区二区三区自拍偷拍| 欧美老人巨大XXXX做受视频| 久久婷婷五月综合97色直播| 国产精品污一区二区三区| 欧美日韩一线| 精品一卡2卡三卡4卡乱码精品视频| 国产99视频精品免费视频36| 亚洲中文久久久精品无码| 国产精品黄在线观看免费| 影音先锋大黄瓜视频| 蜜桃一区二区三区在线看| 中文 在线 日韩 亚洲 欧美| 苏尼特右旗| 亚洲成年av天堂动漫网站|