数据库/sql事务 - 检测提交或回滚

53
使用database/sql和driver包和Tx,似乎无法检测事务是否已提交或者回滚,而不需要再尝试一次并作为结果收到错误,然后检查错误以确定错误类型。我想能够从Tx对象中确定是否已提交。当然,我可以在使用Tx的函数中定义和设置另一个变量,但我有相当多的这样的函数,每次都是两倍(变量和分配)。我还有一个延迟函数来执行回滚,如果需要,则需要将bool变量传递给它。将Tx变量设置为nil是否可接受,在提交或回滚后GC会恢复任何内存,还是这样做是不好的,或者是否有更好的替代方案?

1
不确定我是否理解了问题。您必须使用Commit或Rollback结束事务,以便知道自己所做的事情,但您不想在额外的变量中记住这一点?您可以将Tx和bool包装在自己的RememberingTx中,这将稍微减少行数。关于GC问题:无论您设置为nil还是不设置,内存都会在没有引用时被回收。因此:是的,您可以有var tx *Tx; snip; if cond { tx.Commit; tx=nil } else { tx.Rollback}; snip; if tx==nil { was commited } else { was rollbacked},但感觉很丑陋。 - Volker
这有点是它的意思,但如果Tx不为nil,则有一个延迟的函数执行回滚。一旦事务提交,Tx就无法再使用了,因此我计划将其设置为nil。这不太好看,但尝试回滚并测试错误消息也不太好看。问题在于据我所知,没有办法从Tx中测试事务是否“完成”。我不确定为什么会这样做,可能是出于性能考虑。 - Brian Oh
1个回答

163
你需要确保Begin()Commit()Rollback()出现在同一个函数中。这样可以更容易地跟踪事务,并通过使用defer确保它们被正确关闭。
以下是一个示例,根据返回的错误进行提交或回滚:
func (s Service) DoSomething() (err error) {
    tx, err := s.db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()
    if _, err = tx.Exec(...); err != nil {
        return
    }
    if _, err = tx.Exec(...); err != nil {
        return
    }
    // ...
    return
}

这可能会变得有些重复。另一种做法是使用事务处理程序来包装您的交易:
func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return
    }
    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p) // re-throw panic after Rollback
        } else if err != nil {
            tx.Rollback() // err is non-nil; don't change it
        } else {
            err = tx.Commit() // err is nil; if Commit returns error update err
        }
    }()
    err = txFunc(tx)
    return err
}

使用上述事务处理程序,我可以这样做:
func (s Service) DoSomething() error {
    return Transact(s.db, func (tx *sql.Tx) error {
        if _, err := tx.Exec(...); err != nil {
            return err
        }
        if _, err := tx.Exec(...); err != nil {
            return err
        }
        return nil
    })
}

这使得我的交易简明,并确保我的交易得到正确处理。
在我的交易处理程序中,我使用recover()捕获恐慌以确保回滚立即发生。如果预期发生恐慌,我将重新抛出恐慌以允许我的代码捕获它。在正常情况下,不应该发生恐慌。应该返回错误。
如果我们没有处理恐慌,事务最终将被回滚。当客户端断开连接或事务被垃圾回收时,非提交事务由数据库回滚。然而,等待事务自行解决可能会导致其他(未定义)问题。所以最好尽快解决它。
一个可能不立即清楚的问题是,如果捕获变量,在闭包中defer可以更改返回值。在事务处理程序中,当err(返回值)为nil时,事务将被提交。对Commit的调用也可能返回一个错误,所以我们使用err = tx.Commit()将其返回值设置为err。我们不对Rollback执行相同操作,因为err是非空的,我们不想覆盖现有错误。

不错的回答!我认为你在第二个doSomething()实现的末尾漏掉了一个"return nil"。 - splinter123
1
@ChadGilbert,虽然最近的编辑使读者更清楚地了解了正在发生的事情,但我想让您知道它之前就已经起作用了。return设置了err变量。请参见https://play.golang.org/p/66wWTJl0pH。 - Luke
@DaveC 我同意。我更新了示例,在回滚后重新抛出 panic。往往情况下,panic 是由于编程错误引起的,应该进行纠正。http 服务器也会吞噬 panic,但最终会拆除整个请求。在这种情况下,最好在其他地方处理 panic。 - Luke
@DaveC我真的很好奇为什么会这样。我是Go的新手,目前正在研究错误处理。在我看来,在Web应用程序中似乎没有很多情况下需要使用panic,即使出现错误也通常会发送响应,为什么需要终止呢?请告诉我您认为这是一个不良的设计或习惯的原因。 - Tiega
1
@Ratatouille Rollback和Commit语句不应该引起恐慌。recover()语句旨在捕获事务内代码引起的恐慌。理想情况下,您永远不会发生恐慌-您将返回一个错误,这也会导致回滚。如果既没有调用Rollback也没有调用Commit,并且事务从未解决(发生断开连接或事务被垃圾回收),则db将执行回滚。如果有错误,则Commit会返回一个错误(您可以在return完成之前在defer中更改err)。Rollback不会覆盖导致其发生的现有错误。 - Luke
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接