5

为了更好地学习 Go,我试图将一系列接受 DB 连接作为第一个参数的函数重构为 struct 方法和一些更“惯用”的 Go。

现在我的“数据存储”方法是这样的:

func CreateA(db orm.DB, a *A) error {
    db.Exec("INSERT...")
}

func CreateB(db orm.DB, b *B) error {
    db.Exec("INSERT...")
}

这些功能工作得很好。orm.DBgo-pg 的 DB 接口

由于这两个函数接受一个数据库连接,我可以传递一个实际的连接或一个事务(实现相同的接口)。我可以确定发出 SQL INSERT 的两个函数在同一个事务中运行,避免在其中一个失败时在数据库中出现不一致的状态。

当我决定阅读更多关于如何更好地构建代码并使其“可模拟”以备不时之需时,麻烦就开始了。

所以我搜索了一下,阅读了Go 中的实用持久性文章:组织数据库访问并尝试重构代码以使用正确的接口。

结果是这样的:

type Store {
    CreateA(a *A) error
    CreateB(a *A) error
}

type DB struct {
    orm.DB
}

func NewDBConnection(p *ConnParams) (*DB, error) {
    .... create db connection ...
    return &DB{db}, nil
}

func (db *DB) CreateA(a *A) error {
...
}

func (db *DB) CreateB(b *B) error {
...
}

这使我可以编写如下代码:

db := NewDBConnection()
DB.CreateA(a)
DB.CreateB(b)

代替:

db := NewDBConnection()
CreateA(db, a)
CreateB(db, b)

实际问题是我失去了在同一个事务中运行这两个函数的能力。在我做之前:

pgDB := DB.DB.(*pg.DB) // convert the interface to an actual connection
pgDB.RunInTransaction(func(tx *pg.Tx) error {
    CreateA(tx, a)
    CreateB(tx, b)
})

或类似的东西:

tx := db.DB.Begin()

err = CreateA(tx, a)
err = CreateB(tx, b)

if err != nil {
  tx.Rollback()
} else {
  tx.Commit()
}

这或多或少是一回事。

由于函数接受连接和事务之间的公共接口,我可以从模型层抽象事务逻辑发送完整连接或事务。这让我可以在“HTTP 处理程序”中决定何时创建事务以及何时不需要。

请记住,连接是一个全局对象,表示 go 自动处理的连接池,所以我尝试了以下技巧:

pgDB := DB.DB.(*pg.DB) // convert the interface to an actual connection
err = pgDB.RunInTransaction(func(tx *pg.Tx) error {
    DB.DB = tx // replace the connection with a transaction
    DB.CreateA(a)
    DB.CreateB(a)
})

这显然是个坏主意,因为虽然它有效,但它只有效一次,因为我们用事务替换了全局连接。以下请求会中断服务器。

有任何想法吗?我找不到有关此的信息,可能是因为我不知道正确的关键字是菜鸟。

4

1 回答 1

5

我过去做过这样的事情(使用标准sql包,您可能需要根据您的需要对其进行调整):

var ErrNestedTransaction = errors.New("nested transactions are not supported")

// abstraction over sql.TX and sql.DB
// a similar interface seems to be already defined in go-pg. So you may not need this. 
type executor interface {
    Exec(query string, args ...interface{}) (sql.Result, error)
    Query(query string, args ...interface{}) (*sql.Rows, error)
    QueryRow(query string, args ...interface{}) *sql.Row
}

type Store struct {
    // this is the actual connection(pool) to the db which has the Begin() method
    db       *sql.DB
    executor executor
}

func NewStore(dsn string) (*Store, error) {
    db, err := sql.Open("sqlite3", dsn)
    if err != nil {
         return nil, err
    }      
    // the initial store contains just the connection(pool)
    return &Store{db, db}, nil
}

func (s *Store) RunInTransaction(f func(store *Store) error) error {
    if _, ok := s.executor.(*sql.Tx); ok {
        // nested transactions are not supported!
        return ErrNestedTransaction
    }

    tx, err := s.db.Begin()
    if err != nil {
        return err
    }

    transactedStore := &Store{
        s.db,
        tx,
    }

    err = f(transactedStore)
    if err != nil {
        tx.Rollback()
        return err
    }

    return tx.Commit()
}

func (s *Store) CreateA(thing A) error {
    // your implementation
    _, err := s.executor.Exec("INSERT INTO ...", ...)
    return err
}

然后你像这样使用它

// store is a global object
store.RunInTransaction(func(store *Store) error { 
    // this instance of Store uses a transaction to execute the methods
    err := store.CreateA(a)
    if err != nil {
        return err
    }
    return store.CreateB(b)
})

诀窍是在您的 CreateX 方法中使用执行程序而不是 *sql.DB,这允许您动态更改底层实现(tx 与 db)。但是,由于关于如何处理此问题的信息很少,我不能向您保证这是“最佳”解决方案。欢迎其他建议!

于 2018-03-31T23:51:33.780 回答