新聞中心
使用?GOFrame ORM?組件進(jìn)行事務(wù)操作非常簡(jiǎn)便、安全,可以通過兩種操作方式來實(shí)現(xiàn)。

我們提供的服務(wù)有:成都網(wǎng)站建設(shè)、成都做網(wǎng)站、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、廣西ssl等。為上1000家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的廣西網(wǎng)站制作公司
- 常規(guī)操作:通過?
Begin?開啟事務(wù)之后會(huì)返回一個(gè)事務(wù)操作對(duì)象?*gdb.TX?,隨后可以使用該對(duì)象進(jìn)行如之前章節(jié)介紹的方法操作和鏈?zhǔn)讲僮?。常?guī)操作容易漏掉關(guān)閉事務(wù),有一定的事務(wù)操作安全風(fēng)險(xiǎn)。 - 閉包操作:通過?
Transaction?閉包方法的形式來操作事務(wù),所有的事務(wù)邏輯在閉包中實(shí)現(xiàn),閉包結(jié)束后自動(dòng)關(guān)閉事務(wù)保障事務(wù)操作安全。并且閉包操作支持非常便捷的嵌套事務(wù),嵌套事務(wù)在業(yè)務(wù)操作中透明無感知。
我們推薦事務(wù)操作均統(tǒng)一采用?Transaction?閉包方式實(shí)現(xiàn)。
接口文檔: https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#TX
常規(guī)事務(wù)方法
常規(guī)的事務(wù)操作方法為?Begin/Commit/Rollback?,每一個(gè)方法指定特定的事務(wù)操作。開啟事務(wù)操作可以通過執(zhí)行?db.Begin?方法,該方法返回事務(wù)的操作對(duì)象,類型為?*gdb.Tx?,通過該對(duì)象執(zhí)行后續(xù)的數(shù)據(jù)庫操作,并可通過?tx.Commit?提交修改,或者通過?tx.Rollback?回滾修改。
常見問題注意:開啟事務(wù)操作后,請(qǐng)務(wù)必在不需要使用該事務(wù)對(duì)象時(shí),通過?Commit/Rollback?操作關(guān)閉掉該事務(wù),建議充分利用好?defer?方法。如果事務(wù)使用后不關(guān)閉,在應(yīng)用側(cè)會(huì)引起?goroutine?不斷激增泄露,在數(shù)據(jù)庫側(cè)會(huì)引起事務(wù)線程數(shù)量被打滿,以至于后續(xù)的事務(wù)請(qǐng)求執(zhí)行超時(shí)。此外,建議盡可能使用后續(xù)介紹的?Transaction?閉包方法來安全實(shí)現(xiàn)事務(wù)操作。
1. 開啟事務(wù)操作
if tx, err := db.Begin(ctx); err == nil {
fmt.Println("開啟事務(wù)操作")
}事務(wù)操作對(duì)象可以執(zhí)行所有?db?對(duì)象的方法。
2. 事務(wù)回滾操作
if tx, err := db.Begin(ctx); err == nil {
r, err := tx.Save("user", g.Map{
"id" : 1,
"name" : "john",
})
if err != nil {
tx.Rollback()
}
fmt.Println(r)
}
3. 事務(wù)提交操作
if tx, err := db.Begin(ctx); err == nil {
r, err := tx.Save("user", g.Map{
"id" : 1,
"name" : "john",
})
if err == nil {
tx.Commit()
}
fmt.Println(r)
}
4. 事務(wù)鏈?zhǔn)讲僮?
事務(wù)操作對(duì)象仍然可以通過?tx.Model?方法返回一個(gè)鏈?zhǔn)讲僮鞯膶?duì)象,該對(duì)象與?db.Model?方法返回值相同,只不過數(shù)據(jù)庫操作在事務(wù)上執(zhí)行,可提交或回滾。
if tx, err := db.Begin(); err == nil {
r, err := tx.Model("user").Data(g.Map{"id":1, "name": "john_1"}).Save()
if err == nil {
tx.Commit()
}
fmt.Println(r)
}
Transaction閉包操作
可以看到,通過常規(guī)的事務(wù)方法來管理事務(wù)有很多重復(fù)性的操作,并且存在遺忘提交/回滾操作來關(guān)閉事務(wù)的風(fēng)險(xiǎn),因此為方便安全執(zhí)行事務(wù)操作,?ORM?組件同樣提供了事務(wù)的閉包操作,通過?Transaction?方法實(shí)現(xiàn),該方法定義如下:
func (db DB) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error)當(dāng)給定的閉包方法返回的?error?為?nil?時(shí),那么閉包執(zhí)行結(jié)束后當(dāng)前事務(wù)自動(dòng)執(zhí)行?Commit?提交操作;否則自動(dòng)執(zhí)行?Rollback?回滾操作。閉包中的?context.Context?參數(shù)為?GoFrame v1.16?版本后新增的上下文變量,主要用于鏈路跟蹤傳遞以及嵌套事務(wù)管理。由于上下文變量是嵌套事務(wù)管理的重要參數(shù),因此上下文變量通過顯示的參數(shù)傳遞定義。
如果閉包內(nèi)部操作產(chǎn)生?panic?中斷,該事務(wù)也將自動(dòng)進(jìn)行回滾,以保證操作安全。
使用示例:
db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
// user
result, err := tx.Ctx(ctx).Insert("user", g.Map{
"passport": "john",
"password": "12345678",
"nickname": "JohnGuo",
})
if err != nil {
return err
}
// user_detail
id, err := result.LastInsertId()
if err != nil {
return err
}
_, err = tx.Ctx(ctx).Insert("user_detail", g.Map{
"uid": id,
"site": "https://johng.cn",
"true_name": "GuoQiang",
})
if err != nil {
return err
}
return nil
})
Transaction嵌套事務(wù)
從?GoFrame ORM?支持?jǐn)?shù)據(jù)庫嵌套事務(wù)。需要注意的是,數(shù)據(jù)庫服務(wù)往往并不支持嵌套事務(wù),而是依靠?ORM?組件層通過?Transaction Save Point?特性實(shí)現(xiàn)的。相關(guān)方法:
// 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?閉包方法來實(shí)現(xiàn)嵌套事務(wù)操作。為了保證文檔的完整性,因此我們這里仍然從最基本的事務(wù)操作方法開始來介紹嵌套事務(wù)操作。
1. 示例SQL
一個(gè)簡(jiǎn)單的示例?SQL?,包含兩個(gè)字段?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;
2. 常規(guī)操作
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)
}
db.Begin與tx.Begin
可以看到,在我們的嵌套事務(wù)中出現(xiàn)了?db.Begin?和?tx.Begin?兩種事務(wù)開啟方式,兩者有什么區(qū)別呢??db.Begin?是在數(shù)據(jù)庫服務(wù)上真正開啟一個(gè)事務(wù)操作,并返回一個(gè)事務(wù)操作對(duì)象?tx?,隨后所有的事務(wù)操作都是通過該?tx?事務(wù)對(duì)象來操作管理。?tx.Begin?表示在當(dāng)前事務(wù)操作中開啟嵌套事務(wù),默認(rèn)情況下會(huì)對(duì)嵌套事務(wù)的?SavePoint?采用自動(dòng)命名,命名格式為?transactionN?,其中的?N?表示嵌套的層級(jí)數(shù)量,如果您看到日志中出現(xiàn)?SAVEPOINT `transaction1`?表示當(dāng)前嵌套層級(jí)為2(從0開始計(jì)算)。
更詳細(xì)的日志
?goframe?的?ORM?擁有相當(dāng)完善的日志記錄機(jī)制,如果您打開?SQL?日志,那么將會(huì)看到以下日志信息,展示了整個(gè)數(shù)據(jù)庫請(qǐng)求的詳細(xì)執(zhí)行流程:
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?組件記錄的事務(wù)?ID?,多個(gè)真實(shí)的事務(wù)同時(shí)操作時(shí),每個(gè)事務(wù)的?ID?將會(huì)不同。在同一個(gè)真實(shí)事務(wù)下的嵌套事務(wù)的事務(wù)?ID?是一樣的。
執(zhí)行后查詢數(shù)據(jù)庫結(jié)果:
mysql> select * from `user`;
+----+-------+
| id | name |
+----+-------+
| 2 | smith |
+----+-------+
1 row in set (0.00 sec)可以看到第一個(gè)操作被成功回滾,只有第二個(gè)操作執(zhí)行并提交成功。
3. 閉包操作(推薦)
我們也可以通過閉包操作來實(shí)現(xiàn)嵌套事務(wù),同樣也是通過?Transaction?方法實(shí)現(xiàn)。
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
})嵌套事務(wù)的閉包嵌套中也可以不使用其中的?tx?對(duì)象,而是直接使用?db?對(duì)象或者?dao?包,這種方式更常見一些。特別是在方法層級(jí)調(diào)用時(shí),使得對(duì)于開發(fā)者來說并不用關(guān)心?tx?對(duì)象的傳遞,也并不用關(guān)心當(dāng)前事務(wù)是否需要嵌套執(zhí)行,一切都由組件自動(dòng)維護(hù),極大減少開發(fā)者的心智負(fù)擔(dān)。但是務(wù)必記得將?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
})如果您打開?SQL?日志,那么執(zhí)行后將會(huì)看到以下日志信息,展示了整個(gè)數(shù)據(jù)庫請(qǐng)求的詳細(xì)執(zhí)行流程:
2021-05-22 21:18:46.672 [DEBU] [ 2 ms] [default] [txid:1] BEGIN
2021-05-22 21:18:46.672 [DEBU] [ 0 ms] [default] [txid:1] SAVEPOINT `transaction0`
2021-05-22 21:18:46.673 [DEBU] [ 0 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user`
2021-05-22 21:18:46.674 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john')
2021-05-22 21:18:46.674 [DEBU] [ 0 ms] [default] [txid:1] RELEASE SAVEPOINT `transaction0`
2021-05-22 21:18:46.675 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `transaction0`
2021-05-22 21:18:46.675 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`name`,`id`) VALUES('smith',2)
2021-05-22 21:18:46.675 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `transaction0`
2021-05-22 21:18:46.676 [DEBU] [ 1 ms] [default] [txid:1] ROLLBACK假如?ctx?上下文變量沒有層層傳遞下去,那么嵌套事務(wù)將會(huì)失敗,我們來看一個(gè)錯(cuò)誤的例子:
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).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
})打開?SQL?執(zhí)行日志,執(zhí)行后,您將會(huì)看到以下日志內(nèi)容:
2021-05-22 21:29:38.841 [DEBU] [ 3 ms] [default] [txid:1] BEGIN
2021-05-22 21:29:38.842 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `transaction0`
2021-05-22 21:29:38.843 [DEBU] [ 1 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user`
2021-05-22 21:29:38.845 [DEBU] [ 2 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john')
2021-05-22 21:29:38.845 [DEBU] [ 0 ms] [default] [txid:1] RELEASE SAVEPOINT `transaction0`
2021-05-22 21:29:38.846 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `transaction0`
2021-05-22 21:29:38.847 [DEBU] [ 1 ms] [default] INSERT INTO `user`(`id`,`name`) VALUES(2,'smith')
2021-05-22 21:29:38.848 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `transaction0`
2021-05-22 21:29:38.848 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK可以看到,第二條?INSERT?操作?INSERT INTO `user`(`id`,`name`) VALUES(2,'smith')? 沒有事務(wù)?ID?打印,表示沒有使用到事務(wù),那么該操作將會(huì)被真正提交到數(shù)據(jù)庫執(zhí)行,并不能被回滾。
4. SavePoint/RollbackTo
開發(fā)者也可以靈活使用?Transaction Save Point?特性,并實(shí)現(xiàn)自定義的?SavePoint?命名以及指定?Point?回滾操作。
tx, err := db.Begin()
if err != nil {
panic(err)
}
defer func() {
if err := recover(); err != nil {
_ = tx.Rollback()
}
}()
if _, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert(); err != nil {
panic(err)
}
if err = tx.SavePoint("MyPoint"); err != nil {
panic(err)
}
if _, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert(); err != nil {
panic(err)
}
if _, err = tx.Model(table).Data(g.Map{"id": 3, "name": "green"}).Insert(); err != nil {
panic(err)
}
if err = tx.RollbackTo("MyPoint"); err != nil {
panic(err)
}
if err = tx.Commit(); err != nil {
panic(err)
}如果您打開?SQL?日志,那么將會(huì)看到以下日志信息,展示了整個(gè)數(shù)據(jù)庫請(qǐng)求的詳細(xì)執(zhí)行流程:
2021-05-22 21:38:51.992 [DEBU] [ 3 ms] [default] [txid:1] BEGIN
2021-05-22 21:38:52.002 [DEBU] [ 9 ms] [default] [txid:1] SHOW FULL COLUMNS FROM `user`
2021-05-22 21:38:52.002 [DEBU] [ 0 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(1,'john')
2021-05-22 21:38:52.003 [DEBU] [ 1 ms] [default] [txid:1] SAVEPOINT `MyPoint`
2021-05-22 21:38:52.004 [DEBU] [ 1 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(2,'smith')
2021-05-22 21:38:52.005 [DEBU] [ 1 ms] [default] [txid:1] INSERT INTO `user`(`id`,`name`) VALUES(3,'green')
2021-05-22 21:38:52.006 [DEBU] [ 0 ms] [default] [txid:1] ROLLBACK TO SAVEPOINT `MyPoint`
2021-05-22 21:38:52.006 [DEBU] [ 0 ms] [default] [txid:1] COMMIT執(zhí)行后查詢數(shù)據(jù)庫結(jié)果:
mysql> select * from `user`;
+----+------+
| id | name |
+----+------+
| 1 | john |
+----+------+
1 row in set (0.00 sec)可以看到,通過在第一個(gè)?Insert?操作后保存了一個(gè)?SavePoint?名稱?MyPoint?,隨后的幾次操作都通過?RollbackTo?方法被回滾掉了,因此只有第一次?Insert?操作被成功提交執(zhí)行。
嵌套事務(wù)在工程中的參考示例
為了簡(jiǎn)化示例,我們還是使用用戶模塊相關(guān)的示例,例如用戶注冊(cè),通過事務(wù)操作保存用戶基本信息(?user?)、詳細(xì)信息(?user_detail?)兩個(gè)表,任一個(gè)表操作失敗整個(gè)注冊(cè)操作都將失敗。為展示嵌套事務(wù)效果,我們將用戶基本信息管理和用戶詳細(xì)信息管理劃分為了兩個(gè)?dao?對(duì)象。
假如我們的項(xiàng)目按照?goframe?標(biāo)準(zhǔn)項(xiàng)目工程化分為三層?api-service-dao?,那么我們的嵌套事務(wù)操作可能是這樣的。
controller
// 用戶注冊(cè)HTTP接口
func (*cUser) Signup(r *ghttp.Request) {
// ....
service.User().Signup(r.Context(), userServiceSignupReq)
// ...
}承接?HTTP?請(qǐng)求,并且將?Context?上下文邊變量傳遞給后續(xù)的流程。
service
// 用戶注冊(cè)業(yè)務(wù)邏輯處理
func (*userService) Signup(ctx context.Context, r *model.UserServiceSignupReq) {
// ....
dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
err := dao.User.Ctx(ctx).Save(r.UserInfo)
if err != nil {
return err
}
err := dao.UserDetail.Ctx(ctx).Save(r.UserDetail)
if err != nil {
return err
}
return nil
})
// ...
}可以看到,內(nèi)部的?user?表和?user_detail?表使用了嵌套事務(wù)來統(tǒng)一執(zhí)行事務(wù)操作。注意在閉包內(nèi)部需要通過?Ctx?方法將上下文變量傳遞給下一層級(jí)。假如在閉包中存在對(duì)其他?service?對(duì)象的調(diào)用,那么也需要將?ctx?變量傳遞過去,例如:
func (*userService) Signup(ctx context.Context, r *model.UserServiceSignupReq) {
// ....
dao.User.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) (err error) {
if err = dao.User.Ctx(ctx).Save(r.UserInfo); err != nil {
return err
}
if err = dao.UserDetail.Ctx(ctx).Save(r.UserDetail); err != nil {
return err
}
if err = service.XXXA().Call(ctx, ...); err != nil {
return err
}
if err = service.XXXB().Call(ctx, ...); err != nil {
return err
}
if err = service.XXXC().Call(ctx, ...); err != nil {
return err
}
// ...
return nil
})
// ...
}
dao
?dao?層的代碼由?goframe cli?工具全自動(dòng)化生成及維護(hù)即可。
分享題目:創(chuàng)新互聯(lián)GoFrame教程:GoFrame數(shù)據(jù)庫ORM-事務(wù)處理
網(wǎng)站網(wǎng)址:http://m.fisionsoft.com.cn/article/cccochp.html


咨詢
建站咨詢
