从 GoFrame ORM
支持数据库嵌套事务。需要注意的是,数据库服务往往并不支持嵌套事务,而是依靠 ORM
组件层通过 Transaction Save Point
特性实现的。相关方法:
// Begin starts a nested transaction procedure.
func (tx *TX) Begin() error
// Commit commits current transaction.
// Note that it releases previous saved transaction point if it's in a nested transaction procedure,
// or else it commits the hole transaction.
func (tx *TX) Commit() error
// Rollback aborts current transaction.
// Note that it aborts current transaction if it's in a nested transaction procedure,
// or else it aborts the hole transaction.
func (tx *TX) Rollback() error
// SavePoint performs `SAVEPOINT xxx` SQL statement that saves transaction at current point.
// The parameter `point` specifies the point name that will be saved to server.
func (tx *TX) SavePoint(point string) error
// RollbackTo performs `ROLLBACK TO SAVEPOINT xxx` SQL statement that rollbacks to specified saved transaction.
// The parameter `point` specifies the point name that was saved previously.
func (tx *TX) RollbackTo(point string) error
// Transaction wraps the transaction logic using function `f`.
// It rollbacks the transaction and returns the error from function `f` if
// it returns non-nil error. It commits the transaction and returns nil if
// function `f` returns nil.
//
// Note that, you should not Commit or Rollback the transaction in function `f`
// as it is automatically handled by this function.
func (tx *TX) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error)
同样的,我们推荐使用 Transaction
闭包方法来实现嵌套事务操作。为了保证文档的完整性,因此我们这里仍然从最基本的事务操作方法开始来介绍嵌套事务操作。
一、示例SQL
一个简单的示例 SQL
,包含两个字段 id
和 name
:
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL COMMENT '用户ID',
`name` varchar(45) NOT NULL COMMENT '用户名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
二、常规操作
db := g.DB()
tx, err := db.Begin()
if err != nil {
panic(err)
}
if err = tx.Begin(); err != nil {
panic(err)
}
_, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert()
if err = tx.Rollback(); err != nil {
panic(err)
}
_, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert()
if err = tx.Commit(); err != nil {
panic(err)
}
1、 db.Begin
与 tx.Begin
可以看到,在我们的嵌套事务中出现了 db.Begin
和 tx.Begin
两种事务开启方式,两者有什么区别呢? db.Begin
是在数据库服务上真正开启一个事务操作,并返回一个事务操作对象 tx
,随后所有的事务操作都是通过该 tx
事务对象来操作管理。 tx.Begin
表示在当前事务操作中开启嵌套事务,默认情况下会对嵌套事务的 SavePoint
采用自动命名,命名格式为 transactionN
,其中的 N
表示嵌套的层级数量,如果您看到日志中出现 SAVEPOINT `transaction1`
表示当前嵌套层级为 2
(从 0
开始计算)。
2、更详细的日志
goframe
的 ORM
拥有相当完善的日志记录机制,如果您打开 SQL
日志,那么将会看到以下日志信息,展示了整个数据库请求的详细执行流程:
2021-05-22 21:12:10.776 [DEBU] [ 4 ms] [default] [txid:1] BEGIN
2021-05-22 21:12:10.776 [DEBU] [ 0 ms] [default] [txid:1] SAVEPOINT `transaction0`
2021-05-22 21:12:10.789 [DEBU] [ 13 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user`
2021-05-22 21:12:10.790 [DEBU] [ 1 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john')
2021-05-22 21:12:10.791 [DEBU] [ 1 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `transaction0`
2021-05-22 21:12:10.791 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(2,'smith')
2021-05-22 21:12:10.792 [DEBU] [ 1 ms] [default] [txid:1] COMMIT
其中的 [txid:1]
表示 ORM
组件记录的事务ID,多个真实的事务同时操作时,每个事务的ID将会不同。在同一个真实事务下的嵌套事务的事务ID是一样的。
执行后查询数据库结果:
mysql> select * from `user`;
+----+-------+
| id | name |
+----+-------+
| 2 | smith |
+----+-------+
1 row in set (0.00 sec)
可以看到第一个操作被成功回滚,只有第二个操作执行并提交成功。
三、闭包操作(推荐)
我们也可以通过闭包操作来实现嵌套事务,同样也是通过 Transaction
方法实现。
db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
// Nested transaction 1.
if err := tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
_, err := tx.Model(table).Ctx(ctx).Data(g.Map{"id": 1, "name": "john"}).Insert()
return err
}); err != nil {
return err
}
// Nested transaction 2, panic.
if err := tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
_, err := tx.Model(table).Ctx(ctx).Data(g.Map{"id": 2, "name": "smith"}).Insert()
// Create a panic that can make this transaction rollback automatically.
panic("error")
return err
}); err != nil {
return err
}
return nil
})
嵌套事务的闭包嵌套中也可以不使用其中的 tx
对象,而是直接使用 db
对象或者 dao
包,这种方式更常见一些。特别是在方法层级调用时,使得对于开发者来说并不用关心 tx
对象的传递,也并不用关心当前事务是否需要嵌套执行,一切都由组件自动维护,极大减少开发者的心智负担。但是务必记得将 ctx
上下文变量层层传递下去哦。例如:
db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
// Nested transaction 1.
if err := db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
_, err := db.Model(table).Ctx(ctx).Data(g.Map{"id": 1, "name": "john"}).Insert()
return err
}); err != nil {
return err
}
// Nested transaction 2, panic.
if err := db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
_, err := db.Model(table).Ctx(ctx).Data(g.Map{"id": 2, "name": "smith"}).Insert()
// Create a panic that can make this transaction rollback automatically.
panic("error")
return err
}); err != nil {
return err
}
return nil
})