数据库/sql Tx – 检测提交或回滚

huangapple go评论81阅读模式
英文:

database/sql Tx - detecting Commit or Rollback

问题

使用database/sql和driver包以及Tx,似乎无法在不尝试另一个事务并接收错误结果的情况下检测事务是否已提交或回滚,然后检查错误以确定错误类型。我希望能够从Tx对象中确定是否已提交。当然,我可以在使用Tx的函数中定义和设置另一个变量,但我有很多这样的函数,每次都是两倍的时间(变量和赋值)。我还有一个延迟函数来执行回滚(如果需要),它需要传递布尔变量。<br><br>
在提交或回滚后将Tx变量设置为nil是否可行,GC会回收任何内存,还是这是不可行的,或者有更好的替代方案?

英文:

Using the database/sql and driver packages and Tx, it is not possible it appears to detect whether a transaction has been committed or rolled-back without attempting another and receiving an error as a result, and then examining the error to determine the type of error. I would like to be able to determine from the Tx object whether committed or not. Sure, I can define and set another variable in the function that uses Tx, but I have quite a number of them, and it is times 2 every time (variable and assignment). I also have a deferred function to do a Rollback if needed, and it needs to be passed the bool variable. <br><br>
Would it be acceptable to set the Tx variable to nil after a Commit or Rollback, and will the GC recover any memory, or is that a no-no, or is there a better alternative?

答案1

得分: 163

你想确保Begin()Commit()Rollback()出现在同一个函数中。这样可以更容易追踪事务,并通过使用defer确保它们被正确关闭。

以下是一个示例,根据返回的错误执行Commit或Rollback:

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) // 在Rollback之后重新抛出panic
        } else if err != nil {
            tx.Rollback() // err非nil,不要更改它
        } else {
            err = tx.Commit() // err为nil,如果Commit返回错误,则更新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()来捕获panic,以确保立即进行Rollback。我重新抛出panic,以便我的代码可以捕获它(如果预期会发生panic)。在正常情况下,不应该发生panic,而应该返回错误。

如果我们不处理panic,事务最终会被回滚。当客户端断开连接或事务被垃圾回收时,未提交的事务会被数据库回滚。然而,等待事务自行解决可能会导致其他(未定义的)问题。因此,最好尽快解决它。

可能不太明显的一点是,如果捕获了返回变量,defer可以在闭包中更改返回值。在事务处理程序中,当err(返回值)为nil时,事务被提交。调用Commit也可能返回错误,因此我们使用err = tx.Commit()将其返回值设置为err。我们不对Rollback做同样的操作,因为err是非nil的,我们不希望覆盖现有的错误。

英文:

You want to make sure that Begin(), Commit(), and Rollback() appear within the same function. It makes transactions easier to track, and lets you ensure they are closed properly by using a defer.

Here is an example of this, which does a Commit or Rollback depending on whether an error is returned:

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
}

This can get a bit repetitive. Another way of doing this is by wrapping your transactions using a transaction handler:

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&#39;t change it
		} else {
		    err = tx.Commit() // err is nil; if Commit returns error update err
		}
	}()
    err = txFunc(tx)
	return err
}

Using the transaction hander above, I can do this:

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
	})
}

This keeps my transactions succinct and ensures by transactions are properly handled.

In my transaction handler I use recover() to catch panics to ensure a Rollback happens right away. I re-throw the panic to allow my code to catch it if a panic is expected. Under normal circumstances a panic should not occur. Errors should be returned instead.

If we did not handle panics the transaction would be rolled back eventually. A non-commited transaction gets rolled back by the database when the client disconnects or when the transaction gets garbage collected. However, waiting for the transaction to resolve on its own could cause other (undefined) issues. So it's better to resolve it as quickly as possible.

One thing that may not be immediately clear is that defer can change the return value within a closure if the return variable is captured. In the transaction handler the transaction is committed when err (the return value) is nil. The call to Commit can also return an error, so we set its return to err with err = tx.Commit(). We do not do the same with Rollback because err is non-nil and we do not want to overwrite the existing error.

huangapple
  • 本文由 发表于 2013年4月24日 13:45:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/16184238.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定