为了更好地学习 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.DB
是go-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)
})
这显然是个坏主意,因为虽然它有效,但它只有效一次,因为我们用事务替换了全局连接。以下请求会中断服务器。
有任何想法吗?我找不到有关此的信息,可能是因为我不知道正确的关键字是菜鸟。