为了账号安全,请及时绑定邮箱和手机立即绑定

Golang 事务性 API 设计

Golang 事务性 API 设计

Go
蝴蝶刀刀 2023-03-21 15:57:35
我正在尝试使用 Go 遵循Clean Architecture。该应用程序是一个简单的图像管理应用程序。我想知道如何最好地为我的存储库层设计接口。我不想将所有存储库方法组合到一个大接口中,就像我发现的一些示例那样,我认为在 Go 中通常首选小接口。我不认为有关管理图像的用例代码需要知道存储库还存储用户。所以我想有UserReader,UserWriter和ImageReader。ImageWriter复杂的是代码需要是事务性的。事务管理属于 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) errorfunc (tx *BoltDBTransaction) WriteUser(i usecase.User) error....不幸的是,如果我实现这样的交易方法:func (r *BoltDBRepository) Writer() *BoltDBTransactionfunc (r *BoltDBRepository) Reader() *BoltDBTransaction因为这没有实现ImageRepository接口,所以我需要一个简单的包装器type ImageRepository struct { *BoltDBRepository }func (ir *ImageRepository) Writer() usecase.ImageReadWriterfunc (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 ImageWriterfunc (repo *BoltDBRepository) WriteImage(tx usecase.Transaction, img usecase.Image) error {  boltTx := tx.(*BoltDBTransaction)  ...}问题:虽然这可行,但我必须在每个存储库方法的开头键入断言,这看起来有点乏味。所以这些是我可以想出的方法。哪个最合适,或者有更好的解决方案?
查看完整描述

3 回答

?
白板的微信

TA贡献1883条经验 获得超3个赞

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


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


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


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)中运行良好!

在洋葱架构中:

  • 实体:用户和图像(没有业务规则)

  • Usecase : 存储库接口(应用规则:保留用户和图像)

  • 控制器: A/N

  • DB/Api:客户端、tx、存储库实现


查看完整回答
反对 回复 2023-03-21
?
哆啦的时光机

TA贡献1779条经验 获得超6个赞

保持存储库原样,不要尝试在那里解决事务性 API 的想法。您需要一个单独的存储库注册表来控制您的存储库将如何初始化以及它们的行为方式;原子操作等这是一个很好的例子:


文件:内部/存储库/registry.go


package repository


import (

    "context"


    "github.com/kataras/app/image"

)


type TransactionFunc = func(Registry) error


type Registry interface {

    NewImageRepository() image.Repository

    // more repo initialization funcs...


    InTransaction(context.Context, func(Registry) error) error

}

文件:内部/存储库/注册表/postgres.go


package registry


import (

    "context"

    "fmt"


    "github.com/kataras/app/image"

    "github.com/kataras/app/internal/repository"


    "github.com/kataras/pg" // your or 3rd-party database driver package.

)


type PostgresRepositoryRegistry struct {

    db *pg.DB

}


var _ repository.Registry = (*PostgresRepositoryRegistry)(nil)


func NewPostgresRepositoryRegistry(db *pg.DB) *PostgresRepositoryRegistry {

    return &PostgresRepositoryRegistry{

        db: db,

    }

}


func (r *PostgresRepositoryRegistry) NewImageRepository() image.Repository {

    return image.NewPostgresRepository(r.db)

}



// The important stuff!

func (r *PostgresRepositoryRegistry) InTransaction(ctx context.Context, fn repository.TransactionFunc) (err error) {

    if r.db.IsTransaction() {

        return fn(r)

    }


    var tx *pg.DB

    tx, err = r.db.BeginDatabase(ctx)

    if err != nil {

        return

    }


    defer func() {

        if p := recover(); p != nil {

            _ = tx.RollbackDatabase(ctx)

            panic(p)

        } else if err != nil {

            rollbackErr := tx.RollbackDatabase(ctx)

            if rollbackErr != nil {

                err = fmt.Errorf("%w: %s", err, rollbackErr.Error())

            }

        } else {

            err = tx.CommitDatabase(ctx)

        }

    }()


    newRegistry := NewPostgresRepositoryRegistry(tx)

    err = fn(newRegistry)


    return

}

现在,在您的域服务级别,您只需注入一个repository.Registry,例如PostgresRepositoryRegistry.


文件:内部/服务/image_service.go


package service


import (

    "context"


    "github.com/kataras/app/internal/repository"

)


type ImageService struct {

    registry repository.Registry

}


func NewImageService (registry repository.Registry) *ImageService {

    return &ImageService {

        registry: registry ,

    }

}


func (s *ImageService) DoSomeWork(ctx context.Context, ...) error {

    images := s.registry.NewImageRepository()

    images.DoSomeWork(ctx, ...)

}


// Important stuff!

func (s *ImageService) DoSomeWorkInTx(ctx context.Context, inputs [T]) error {

    return s.registry.InTransaction(ctx, func(r repository.Registry) error) {

        images := r.NewImageRepository()

        for _, in := range inputs {

            if err := images.DoSomeWork(); err!=nil {

                  return err // rollback.

            }

        }


        return nil

    }


}

在您的 API 路由中使用 ImageService。


db, err := pg.Open(...)

// handleError(err)

repoRegistry := registry.NewPostgresRepositoryRegistry(db)

imageService := service.NewImageService(repoRegistry)


// controller := &MyImageController{Service: imageService}

您可以使用Iris进行依赖注入。


查看完整回答
反对 回复 2023-03-21
?
幕布斯6054654

TA贡献1876条经验 获得超7个赞

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


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()

}



查看完整回答
反对 回复 2023-03-21
  • 3 回答
  • 0 关注
  • 99 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信