为了支持业务层中的事务,我试图在Go中查找类似Spring的声明式事务管理,但是没找到,所以我决定自己写一个。 事务很容易在Go中实现,但很难做到正确地实现。
需求:
- 将业务逻辑与事务代码分开。
在编写业务用例时,开发者应该只需考虑业务逻辑,不需要同时考虑怎样给业务逻辑加事务管理。如果以后需要添加事务支持,你可以在现有业务逻辑的基础上进行简单封装,而无需更改任何其他代码。事务实现细节应该对业务逻辑透明。 - 事务逻辑应该作用于用例层(业务逻辑)
不在持久层上。 - 数据服务(数据持久性)层应对事务逻辑透明。
这意味着持久性代码应该是相同的,无论它是否支持事务 - 你可以选择延迟支持事物。
你可以先编写没有事务的用例,稍后可以在不修改现有代码的情况下给该用例加上事务。你只需添加新代码。
我最终的解决方案还不是声明式事务管理,但它非常接近。创建一个真正的声明式事务管理需要付出很多努力,因此我构建了一个可以实现声明式事务的大多数功能的事务管理,同时又没花很多精力。
方案:
最终解决方案涉及本程序的所有层级,我将逐一解释它们。
数据库链接封装
在Go的“sql”lib中,有两个数据库链接sql.DB和sql.Tx. 不需要事务时,使用sql.DB访问数据库; 当需要事务时,你使用sql.Tx. 为了共享代码,持久层需要同时支持两者。 因此需要对数据库链接进行封装,然后把它作为数据库访问方法的接收器。 我从这里¹得到了粗略的想法。
// SqlGdbc (SQL Go database connection) is a wrapper for SQL database handler ( can be *sql.DB or *sql.Tx)
// It should be able to work with all SQL data that follows SQL standard.
type SqlGdbc interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
// If need transaction support, add this interface
Transactioner
}
// SqlDBTx is the concrete implementation of sqlGdbc by using *sql.DB
type SqlDBTx struct {
DB *sql.DB
}
// SqlConnTx is the concrete implementation of sqlGdbc by using *sql.Tx
type SqlConnTx struct {
DB *sql.Tx
}
数据库实现类型SqlDBTx和sqlConnTx都需要实现SqlGdbc接口(包括“Transactioner”)接口才行。 需要为每个数据库(例如MySQL, CouchDB)实现“Transactioner”接口以支持事务。
// Transactioner is the transaction interface for database handler
// It should only be applicable to SQL database
type Transactioner interface {
// Rollback a transaction
Rollback() error
// Commit a transaction
Commit() error
// TxEnd commits a transaction if no errors, otherwise rollback
// txFunc is the operations wrapped in a transaction
TxEnd(txFunc func() error) error
// TxBegin gets *sql.DB from receiver and return a SqlGdbc, which has a *sql.Tx
TxBegin() (SqlGdbc, error)
}
数据库存储层(datastore layer)的事物管理代码
以下是“Transactioner”接口的实现代码,其中只有TxBegin()是在SqlDBTx(sql.DB)上实现,因为事务从sql.DB开始,然后所有事务的其他操作都在SqlConnTx(sql.Tx)上。 我从这里²得到了这个想法。
// TransactionBegin starts a transaction
func (sdt *SqlDBTx) TxBegin() (gdbc.SqlGdbc, error) {
tx, err := sdt.DB.Begin()
sct := SqlConnTx{tx}
return &sct, err
}
func (sct *SqlConnTx) TxEnd(txFunc func() error) error {
var err error
tx := sct.DB
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // re-throw panic after Rollback
} el