为了更好地学习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
是DB interface of go-pg。
由于这两个函数接受db连接,我可以传递实际连接或transaction(实现相同的接口)。我可以确定发出SQL INSERT的两个函数都在同一个事务中运行,避免DB中的状态不一致,以防其中任何一个失败。
当我决定阅读更多关于如何更好地构建代码并在需要时使其“可模拟”时,麻烦就开始了。
所以我用Google搜索了一下,阅读文章Practical Persistence in Go: Organising Database Access并试图重构代码以使用正确的接口。
结果是这样的:
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处理程序”中决定何时创建trasaction以及何时不需要。
请记住,连接是一个全局对象,表示由go自动处理的连接池,所以我试过这个hack:
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)
})
这显然是一个坏主意,因为虽然它有效,但它只能运行一次,因为我们用事务替换了全局连接。以下请求会破坏服务器。
有什么想法吗?我无法找到有关这方面的信息,可能是因为我不知道正确的关键字是菜鸟。
答案 0 :(得分:2)
过去我做过这样的事情(使用标准的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)。但是,由于关于如何处理这个问题的信息非常少,我不能向你保证这是“最好的”解决方案。欢迎提出其他建议!