Golang 事务 API 设计

Posted

技术标签:

【中文标题】Golang 事务 API 设计【英文标题】:Golang Transactional API design 【发布时间】:2019-01-25 12:38:17 【问题描述】:

我正在尝试使用 Go 来关注 Clean Architecture。该应用程序是一个简单的图像管理应用程序。

我想知道如何最好地为我的存储库层设计接口。我不想将所有存储库方法组合到一个大接口中,就像我发现的一些示例一样,我认为在 Go 中通常首选小接口。我不认为有关管理图像的用例代码需要知道存储库还存储用户。所以我想要UserReaderUserWriterImageReaderImageWriter。复杂之处在于代码需要是事务性的。事务管理在 Clean Architecture 中的归属存在一些争论,但我认为用例层需要能够控制事务。我认为,属于单一交易的是业务规则,而不是技术细节。

现在的问题是,如何构建接口?

功能方法

所以在这种方法中,我打开一个事务,运行提供的函数并在没有错误的情况下提交。

type UserRepository interface 
    func ReadTransaction(txFn func (UserReader) error) error
    func WriteTransaction(txFn func (UserWriter) error) error


type ImageRepository interface 
    func ReadTransaction(txFn func (ImageReader) error) error
    func WriteTransaction(txFn func (ImageWriter) error) error

问题:不,我不能在单个事务中轻松编写用户和图像,我必须为此创建一个额外的UserImageRepository 接口并提供单独的实现。

事务作为存储库

type ImageRepository interface 
    func Writer() ImageReadWriter
    func Reader() ImageReader

我认为这与函数式方法非常相似。它不会解决多个存储库组合使用的问题,但至少可以通过编写一个简单的包装器来实现。

实现可能如下所示:

type BoltDBRepository struct 
type BoltDBTransaction struct  *bolt.Tx 
func (tx *BoltDBTransaction) WriteImage(i usecase.Image) error
func (tx *BoltDBTransaction) WriteUser(i usecase.User) error
....

不幸的是,如果我实现这样的事务方法:

func (r *BoltDBRepository) Writer() *BoltDBTransaction
func (r *BoltDBRepository) Reader() *BoltDBTransaction

因为它没有实现ImageRepository 接口,所以我需要一个简单的包装器

type ImageRepository struct  *BoltDBRepository 
func (ir *ImageRepository) Writer() usecase.ImageReadWriter
func (ir *ImageRepository) Reader() usecase.ImageReader

交易作为价值

type ImageReader interface 
    func WriteImage(tx Transaction, i Image) error


type Transaction interface  
    func Commit() error


type Repository interface 
    func BeginTransaction() (Transaction, error)

存储库实现看起来像这样

type BoltDBRepository struct 
type BoltDBTransaction struct  *bolt.Tx 

// implement ImageWriter
func (repo *BoltDBRepository) WriteImage(tx usecase.Transaction, img usecase.Image) error 
  boltTx := tx.(*BoltDBTransaction)
  ...

问题:虽然这可行,但我必须在每个存储库方法的开头键入断言,这似乎有点乏味。

所以这些是我能想出的方法。哪个最合适,或者有更好的解决方案?

【问题讨论】:

如果非要断言类型,则Transaction接口不完整。 @Peter 它必须是“不完整的”,因为接口不应包含对数据库实现的引用,例如bolt.Tx 我不关注。您必须使调用接口的一部分所需的所有方法。否则,界面的意义何在? 对于用例层,事务基本上是一个令牌,它必须交给存储库层才能做某事。它也可能是interface,为了清楚起见,我只是给它起了个名字。存储库将创建并接受适用于底层数据库系统的令牌。 这个问题真的是特定于语言的吗?在 *** 上有关事务和干净架构的其他问题中,“常见建议”是“工作单元”模式。也许这对你的情况也有帮助? 【参考方案1】:

存储库是保存数据的地方的代表,架构元素也是如此。

事务是解决非功能性要求(原子操作)的技术细节,因此必须像架构元素中的内部引用或私有函数一样使用它。

在这种情况下,如果您的存储库是这样编写的:

type UserRepository interface 
    func Keep(UserData) error
    func Find(UUID) UserData


type ImageRepository interface 
    func Keep(ImageData) error
    func Find(UUID) ImageData

事务性方法是一种实现细节,因此您可以创建 UserRepository 和 ImageRepository 的“实现”,就像内部引用一样使用。

type UserRepositoryImpl struct 
    Tx Transaction


func (r UserRepository) func Keep(UserData) error  return r.Tx.On(...) 
func (r UserRepository) func Find(UUID) UserData  return r.Tx.WithResult(...)

通过这种方式,您也可以将用户和图像保留在一个事务中。

例如,如果客户端引用了 userRepository 和 imageRepository,并且它负责 userData 和 imageData,并且还希望将这两个数据保存在单个事务中,那么:

//open transaction and set in participants
tx := openTransaction()
ur := NewUserRepository(tx)
ir := NewImageRepository(tx)
//keep user and image datas
err0 := ur.Keep(userData)
err1 := ir.Keep(imageData)
//decision
if err0 != nil || err1 != nil 
  tx.Rollback()
  return

tx.Commit()

这是干净、客观的,并且在洋葱架构、DDD 和 3 层架构中运行良好(Martin Fowler)!

在洋葱架构中:

实体:用户和图像(无业务规则) 用例:存储库接口(应用规则:保留用户和图像) 控制器:A/N DB/Api:客户端、tx、存储库实现

【讨论】:

感谢您的回答!我认为这与我的“交易即价值”方法基本相似,但是将交易移动到存储库结构的字段减少了必要的类型断言的数量。您将在哪一层打开/提交交易?因为我仍然认为用例层应该能够控制事务范围,即使实际事务本身是技术性的。 未启用遵循干净架构用户案例层以引用事务(数据库层)。所以 open/commit/rollback 必须在最外层。 嗯,这就是我们似乎有不同意见的地方。我认为对于很多应用程序,您可能可以通过将事务包装在整个用例执行中来解决问题,但是在某些情况下需要更多控制,然后我相信这是业务层决定哪些操作需要是原子的。那似乎也是Uncle Bob's view (search for "transaction" on the page)。 建筑设计可以通过多种方式构建并解决多个问题,但您说:干净的建筑。我想你想研究这些概念。如果不使用这些概念,您可以将事务控制放在任何地方。如果要使用这些概念,则位置位于最外层。因为这个架构的依赖规则,没有外部做不到的内部控制。无论如何,我只是根据这个架构的概念回答了你的问题。谢谢。 我知道这个问题已经过时了,但它们到底应该写在 DDD 的哪一层?在这种情况下,存储库和服务的外部层是什么?因为通常数据库调用将从服务层发送。你能发展一点吗?你会在 service 和 repo 之间创建一个新层吗?【参考方案2】:

如果你回购必须保留一些状态字段

type UserRepositoryImpl struct 
    db Transaction
    someState bool


func (repo *UserRepositoryImpl) WithTx(tx Transaction) *UserRepositoryImpl 
    newRepo := *repo
    repo.db = tx
    return &newRepo


func main() 
    repo := &UserRepositoryImpl 
        db: connectionInit(),
        state: true,
    

    repo.DoSomething()

    tx := openTransaction()
    txrepo := repo.WithTx(tx)

    txrepo.DoSomething()
    txrepo.DoSomethingElse()

【讨论】:

那么你如何回滚或提交呢?

以上是关于Golang 事务 API 设计的主要内容,如果未能解决你的问题,请参考以下文章

golang redis事务 --- 2022-04-03

Golang执行sql事务

Golang之mysql

[go-每日一库]golang-gorm的事务处理

Golang 并发 SQL 事务

带有 golang 准备语句的原始 sql 事务