自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go工程化如何在整潔架構(gòu)中使用事務(wù)?

開發(fā) 后端
事務(wù)的能力是在 repo 上提供的,所以我們需要在 repo 層提供一個(gè)事務(wù)接口,然后在 usecase 中進(jìn)行調(diào)用,保證是事務(wù)執(zhí)行的就行。

[[441824]]

回顧先簡單回顧一下 《Go工程化(九) 項(xiàng)目重構(gòu)實(shí)踐》 如果還沒看過之前這篇文章可以先看一下:

在我們之前的項(xiàng)目目錄分層中,我們主要分為了五個(gè)塊:

  • cmd/appname 是我們服務(wù)的入口,只負(fù)責(zé)啟動(dòng)和依賴注入(使用 Wire)
  • domain 或者 model 是我們的實(shí)體定義 + 接口定義
  • server 負(fù)責(zé)實(shí)現(xiàn)我們在 proto 中定義的接口,在這一層中我們只做數(shù)據(jù)轉(zhuǎn)換,不寫業(yè)務(wù)邏輯
  • usecase 負(fù)責(zé)實(shí)現(xiàn)我們的業(yè)務(wù)邏輯
  • repo 負(fù)責(zé)數(shù)據(jù)操作, 僅做數(shù)據(jù)操作,不實(shí)現(xiàn)業(yè)務(wù)邏輯

在之前的文章中僅僅提到了一個(gè)非常簡單的示例,但是我們實(shí)際業(yè)務(wù)流程往往沒有那么簡單,就一個(gè)非常常見的例子,我們現(xiàn)在需要?jiǎng)?chuàng)建一篇文章,文章上需要關(guān)聯(lián)分類或者是標(biāo)簽信息,這里至少就分兩步:

  • 創(chuàng)建文章
  • 關(guān)聯(lián)文章和標(biāo)簽

這兩個(gè)創(chuàng)建操作需要保證一致性,我們需要在數(shù)據(jù)庫中使用事務(wù),這時(shí)候我們的事務(wù)在哪里承載呢?

在 repo 層承載事務(wù)

其中最簡單也最直接的辦法就是在 repo 的 CreateArticle 方法中我們就使用事務(wù)去同時(shí)創(chuàng)建文章以及標(biāo)簽之間的關(guān)聯(lián)關(guān)系。

  • 我們不是所有的業(yè)務(wù)場景都需要關(guān)聯(lián)創(chuàng)建,有的場景下我們只需要一個(gè)單純的方法又怎么辦呢?
  • 這么寫還有一個(gè)問題,我們把業(yè)務(wù)邏輯下沉到了 repo 中,后面我們還有其它關(guān)聯(lián)也這么搞么?

針對第一個(gè)問題,最簡單的辦法就是我們提供一個(gè) CreateArticleWithTags 方法表示同時(shí)創(chuàng)建這兩者,如果我們需要一個(gè)獨(dú)立的 CreateArticle 再寫一個(gè)就好了。

但是隨著需求越來越多,可能后面還有需要和角色關(guān)聯(lián)的,和商品關(guān)聯(lián)的等等。

難道我們就一種邏輯寫一個(gè)方法么。想想就可怕。

還是在參數(shù)中加上很多可選的 options,然后在一個(gè)方法中不斷判斷。那我們還拿 usecase 做什么直接寫一起不更好么?

在 usecase 層承載事務(wù)

ok,所以直接在 repo 層里面來實(shí)現(xiàn)看上去好像行不通,那我們就把視線往上移動(dòng),我們在 usecase 來解決這個(gè)問題。

事務(wù)的能力是在 repo 上提供的,所以我們需要在 repo 層提供一個(gè)事務(wù)接口,然后在 usecase 中進(jìn)行調(diào)用,保證是事務(wù)執(zhí)行的就行。

使用 repo 層提供的事務(wù)接口

  1. // domain/article.go 
  2. // ArticleRepoTxFunc 事務(wù)方法 
  3. type ArticleRepoTxFunc = func(ctx context.Context, repo IArticleRepo) error 
  4. // IArticleRepo IArticleRepo 
  5. type IArticleRepo interface { 
  6.  Tx(ctx context.Context, f ArticleRepoTxFunc) error 
  7.  GetArticle(ctx context.Context, id int) (*Article, error) 
  8.  CreateArticle(ctx context.Context, article *Article) error 

在 repo 中,我們可以像上面這樣定義,提供一個(gè) Tx 方法,這個(gè)方法接受一個(gè) ArticleRepoTxFunc 作為參數(shù),這個(gè)函數(shù)中的 repo 是開啟了事務(wù)的 repo,通過這個(gè) repo 調(diào)用的所有方法都是在事務(wù)中執(zhí)行的。

  1. // repo/article.go 
  2. func (r *article) Tx(ctx context.Context, f domain.ArticleRepoTxFunc) error { 
  3.  // 注意,這里的 r.db 是 *gorm.DB 
  4.   // 在 gorm 中提供了 Transaction 的工具方法用于執(zhí)行事務(wù),這里我們就不自己寫了 
  5.  return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { 
  6.   // 我們使用事務(wù)的 tx 重新初始化一個(gè) repo 
  7.     // 這個(gè) repo 后續(xù)的執(zhí)行的數(shù)據(jù)庫相關(guān)的操作就都是事務(wù)的了 
  8.   repo := NewArticleRepo(tx) 
  9.   return f(ctx, repo) 
  10.  }) 

然后我們在 usecase 調(diào)用的時(shí)候就可以這樣。

  1. // usecase/article.go 
  2. func (u *article) CreateArticle(ctx context.Context, article *domain.Article, tagIDs []uint) error { 
  3.  return u.repo.Tx(ctx, func(ctx context.Context, repo domain.IArticleRepo) error { 
  4.   err := repo.CreateArticle(ctx, article) 
  5.   if err != nil { 
  6.    return err 
  7.   } 
  8.   var ats []*domain.ArticleTag 
  9.   for _, tid := range tagIDs { 
  10.    ats = append(ats, &domain.ArticleTag{ 
  11.     ArticleID: article.ID, 
  12.     TagID:     tid, 
  13.    }) 
  14.   } 
  15.   return repo.CreateArticleTags(ctx, ats) 
  16.  }) 

這樣寫起來就整潔很多了,業(yè)務(wù)邏輯和我們最初的設(shè)計(jì)一樣,在 usecase 中實(shí)現(xiàn)了,repo 中我們也保持了簡單的原則。

這樣是不是就萬事大吉了呢?如果萬事大吉了這篇文章到這兒也就應(yīng)該結(jié)束了,但是還沒有,說明我在實(shí)踐的過程中還碰到了問題。

問題很簡單,就是我們在 usecase 中不僅僅需要復(fù)用 repo 中的代碼,還有可能需要復(fù)用 usecase 中的代碼,不然我們就可能在 usecase 中出現(xiàn)很多相同的邏輯代碼片段,代碼的重復(fù)率就很高。

我們來看下面一個(gè)例子會(huì)不會(huì)發(fā)現(xiàn)有點(diǎn)什么不對。

  1. // usecase/article.go 
  2. func (u *article) A(ctx contect, args args) error { 
  3.  err := u.CreateArticle(ctx, args.Article) // 包含事務(wù) 
  4.   if err != nil { 
  5.     return err 
  6.   } 
  7.   return u.UpdateXXX(ctx, args.XXX) // 這個(gè)方法中也使用了事務(wù) 

這個(gè)方法內(nèi)其實(shí)是開啟了兩個(gè)事務(wù),這兩個(gè)事務(wù)之間互不相關(guān),不符合我們需求。

在 usecase 層提供事務(wù)方法

  1. // usecase/article.go 
  2. type handler func(ctx context.Context, usecase domain.IArticleUsecase) error 
  3. func (u *article) tx(ctx context.Context, f handler) error { 
  4.  return u.repo.Tx(ctx, func(ctx context.Context, repo domain.IArticleRepo) error { 
  5.   usecase := NewArticleUsecase(repo) 
  6.   return f(ctx, usecase) 
  7.  }) 

我們在 usecase 中也創(chuàng)建了一個(gè) tx 方法,和 repo 類似,在調(diào)用 tx 之后,handler 中的方法的需要都是用新的參數(shù) usecase 這個(gè)新的 usecase 可以保證里面的 repo 調(diào)用都是事務(wù)的。

所以我們之前的 A 函數(shù)可以修改為這樣:

  1. // usecase/article.go 
  2. func (u *article) A(ctx contect, args args) error { 
  3.  return u.tx(ctx, func(ctx context.Context, usecase domain.IArticleUsecase) error { 
  4.   err := usecase.CreateArticle(ctx, args.Article) // 包含事務(wù) 
  5.    if err != nil { 
  6.      return err 
  7.    } 
  8.    return usecase.UpdateXXX(ctx, args.XXX) // 這個(gè)方法中也使用了事務(wù) 
  9.  }) 

這樣就沒有問題了么?我們 UpdateXXX 方法中也調(diào)用 u.tx 方法,這樣就會(huì)導(dǎo)致反復(fù)開啟事務(wù),雖然在 gorm 的 Transaction 方法是支持嵌套事務(wù)的,但是我們還是不要濫用這個(gè)特性。

解決辦法很簡單,我們只需要在執(zhí)行的時(shí)候判斷下就行了。

  1. // usecase/article.go 
  2. type article struct { 
  3.  repo domain.IArticleRepo 
  4.   isTx bool // 用于標(biāo)識(shí)是否開啟了事務(wù) 

然后我們在 tx 方法內(nèi):

  1. func (u *article) tx(ctx context.Context, f handler) error { 
  2.   // 如果已經(jīng)開啟過事務(wù)了我們就直接復(fù)用就行了 
  3.  if u.isTx { 
  4.   return f(ctx, u) 
  5.  } 
  6.  return u.repo.Tx(ctx, func(ctx context.Context, repo domain.IArticleRepo) error { 
  7.   usecase := &article{ 
  8.    repo: repo, 
  9.    isTx: true
  10.   } 
  11.   return f(ctx, usecase) 
  12.  }) 

總結(jié)

文章到這里就到尾聲了,同樣的問題,我們現(xiàn)在這么寫就可以了么?

對于我當(dāng)前所遇到的一些需求來說已經(jīng)可以解決了,當(dāng)然這個(gè)方案并不完美,比如說我們涉及到多個(gè) repo 的時(shí)候,當(dāng)前的方法就沒法直接用了,還得進(jìn)行一些改造,雖然我們要有遠(yuǎn)見但是也不要想的太多,進(jìn)化是優(yōu)于完美的。

 

責(zé)任編輯:姜華 來源: mohuishou
相關(guān)推薦

2021-03-19 07:23:23

Go架構(gòu)Go工程化

2023-06-28 08:25:14

事務(wù)SQL語句

2021-12-27 08:27:18

RepoGo 代碼

2022-04-18 09:41:14

Go架構(gòu)設(shè)計(jì)

2023-09-15 10:33:45

前端工程化commit

2016-02-22 15:02:57

GoRedis連接池

2011-08-10 09:31:41

Hibernateunion

2021-03-09 07:27:40

Kafka開源分布式

2015-08-27 09:46:09

swiftAFNetworkin

2021-06-09 09:36:18

DjangoElasticSearLinux

2022-05-17 08:25:10

TypeScript接口前端

2022-06-23 08:00:53

PythonDateTime模塊

2024-01-18 08:37:33

socketasyncio線程

2019-09-16 19:00:48

Linux變量

2014-07-02 09:47:06

SwiftCocoaPods

2020-04-09 10:18:51

Bash循環(huán)Linux

2024-09-06 11:34:15

RustAI語言

2020-11-30 11:55:07

Docker命令Linux

2019-10-11 10:44:30

Go語言數(shù)據(jù)庫軟件

2023-04-07 11:05:53

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)