在Go中包装db对象并在同一事务中运行两个方法

时间:2018-03-31 22:12:48

标签: go go-pg

为了更好地学习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.DBDB 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)
})

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

有什么想法吗?我无法找到有关这方面的信息,可能是因为我不知道正确的关键字是菜鸟。

1 个答案:

答案 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)。但是,由于关于如何处理这个问题的信息非常少,我不能向你保证这是“最好的”解决方案。欢迎提出其他建议!