Golang 事务性 API 设计

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

我想知道如何最好地为我的存储库层设计接口。我不想将所有存储库方法组合到一个大接口中,就像我发现的一些示例那样,我认为在 Go 中通常首选小接口。我不认为有关管理图像的用例代码需要知道存储库还存储用户。所以我想有UserReader,UserWriterImageReaderImageWriter复杂的是代码需要是事务性的。事务管理属于 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)

  ...

}

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


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


蝴蝶刀刀
浏览 158回答 3
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 participantstx := openTransaction()ur := NewUserRepository(tx)ir := NewImageRepository(tx)//keep user and image dataserr0 := ur.Keep(userData)err1 := ir.Keep(imageData)//decisionif err0 != nil || err1 != nil {  tx.Rollback()  return}tx.Commit()这是干净、客观的,并且在洋葱架构、DDD 和 3 层架构(Martin Fowler)中运行良好!在洋葱架构中:实体:用户和图像(没有业务规则)Usecase : 存储库接口(应用规则:保留用户和图像)控制器: A/NDB/Api:客户端、tx、存储库实现

哆啦的时光机

保持存储库原样,不要尝试在那里解决事务性 API 的想法。您需要一个单独的存储库注册表来控制您的存储库将如何初始化以及它们的行为方式;原子操作等这是一个很好的例子:文件:内部/存储库/registry.gopackage repositoryimport (    "context"    "github.com/kataras/app/image")type TransactionFunc = func(Registry) errortype Registry interface {    NewImageRepository() image.Repository    // more repo initialization funcs...    InTransaction(context.Context, func(Registry) error) error}文件:内部/存储库/注册表/postgres.gopackage registryimport (    "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.gopackage serviceimport (    "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进行依赖注入。

幕布斯6054654

如果你回购必须保留一些状态字段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()}
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go