W3Cschool
恭喜您成為首批注冊用戶
獲得88經驗值獎勵
使用?GoFrame ORM
?組件進行事務操作非常簡便、安全,可以通過兩種操作方式來實現(xiàn)。
Begin
?開啟事務之后會返回一個事務操作對象?*gdb.TX
?,隨后可以使用該對象進行如之前章節(jié)介紹的方法操作和鏈式操作。常規(guī)操作容易漏掉關閉事務,有一定的事務操作安全風險。
Transaction
?閉包方法的形式來操作事務,所有的事務邏輯在閉包中實現(xiàn),閉包結束后自動關閉事務保障事務操作安全。并且閉包操作支持非常便捷的嵌套事務,嵌套事務在業(yè)務操作中透明無感知。我們推薦事務操作均統(tǒng)一采用?Transaction
?閉包方式實現(xiàn)。
接口文檔: https://pkg.go.dev/github.com/gogf/gf/v2/database/gdb#TX
常規(guī)的事務操作方法為?Begin/Commit/Rollback
?,每一個方法指定特定的事務操作。開啟事務操作可以通過執(zhí)行?db.Begin
?方法,該方法返回事務的操作對象,類型為?*gdb.Tx
?,通過該對象執(zhí)行后續(xù)的數據庫操作,并可通過?tx.Commit
?提交修改,或者通過?tx.Rollback
?回滾修改。
常見問題注意:開啟事務操作后,請務必在不需要使用該事務對象時,通過?Commit/Rollback
?操作關閉掉該事務,建議充分利用好?defer
?方法。如果事務使用后不關閉,在應用側會引起?goroutine
?不斷激增泄露,在數據庫側會引起事務線程數量被打滿,以至于后續(xù)的事務請求執(zhí)行超時。此外,建議盡可能使用后續(xù)介紹的?Transaction
?閉包方法來安全實現(xiàn)事務操作。
if tx, err := db.Begin(ctx); err == nil {
fmt.Println("開啟事務操作")
}
事務操作對象可以執(zhí)行所有?db
?對象的方法。
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)
}
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)
}
事務操作對象仍然可以通過?tx.Model
?方法返回一個鏈式操作的對象,該對象與?db.Model
?方法返回值相同,只不過數據庫操作在事務上執(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)
}
可以看到,通過常規(guī)的事務方法來管理事務有很多重復性的操作,并且存在遺忘提交/回滾操作來關閉事務的風險,因此為方便安全執(zhí)行事務操作,?ORM
?組件同樣提供了事務的閉包操作,通過?Transaction
?方法實現(xiàn),該方法定義如下:
func (db DB) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error)
當給定的閉包方法返回的?error
?為?nil
?時,那么閉包執(zhí)行結束后當前事務自動執(zhí)行?Commit
?提交操作;否則自動執(zhí)行?Rollback
?回滾操作。閉包中的?context.Context
?參數為?goframe v1.16
?版本后新增的上下文變量,主要用于鏈路跟蹤傳遞以及嵌套事務管理。由于上下文變量是嵌套事務管理的重要參數,因此上下文變量通過顯示的參數傳遞定義。
如果閉包內部操作產生?panic
?中斷,該事務也將自動進行回滾,以保證操作安全。
使用示例:
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
})
從?GoFrame ORM
?支持數據庫嵌套事務。需要注意的是,數據庫服務往往并不支持嵌套事務,而是依靠?ORM
?組件層通過?Transaction Save Point
?特性實現(xià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
?閉包方法來實現(xiàn)嵌套事務操作。為了保證文檔的完整性,因此我們這里仍然從最基本的事務操作方法開始來介紹嵌套事務操作。
一個簡單的示例?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;
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)
}
可以看到,在我們的嵌套事務中出現(xiàn)了?db.Begin
?和?tx.Begin
?兩種事務開啟方式,兩者有什么區(qū)別呢??db.Begin
?是在數據庫服務上真正開啟一個事務操作,并返回一個事務操作對象?tx
?,隨后所有的事務操作都是通過該?tx
?事務對象來操作管理。?tx.Begin
?表示在當前事務操作中開啟嵌套事務,默認情況下會對嵌套事務的?SavePoint
?采用自動命名,命名格式為?transactionN
?,其中的?N
?表示嵌套的層級數量,如果您看到日志中出現(xiàn)?SAVEPOINT `transaction1`
?表示當前嵌套層級為2(從0開始計算)。
?goframe
?的?ORM
?擁有相當完善的日志記錄機制,如果您打開?SQL
?日志,那么將會看到以下日志信息,展示了整個數據庫請求的詳細執(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
?組件記錄的事務?ID
?,多個真實的事務同時操作時,每個事務的?ID
?將會不同。在同一個真實事務下的嵌套事務的事務?ID
?是一樣的。
執(zhí)行后查詢數據庫結果:
mysql> select * from `user`;
+----+-------+
| id | name |
+----+-------+
| 2 | smith |
+----+-------+
1 row in set (0.00 sec)
可以看到第一個操作被成功回滾,只有第二個操作執(zhí)行并提交成功。
我們也可以通過閉包操作來實現(xiàn)嵌套事務,同樣也是通過?Transaction
?方法實現(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
})
嵌套事務的閉包嵌套中也可以不使用其中的?tx
?對象,而是直接使用?db
?對象或者?dao
?包,這種方式更常見一些。特別是在方法層級調用時,使得對于開發(fā)者來說并不用關心?tx
?對象的傳遞,也并不用關心當前事務是否需要嵌套執(zhí)行,一切都由組件自動維護,極大減少開發(fā)者的心智負擔。但是務必記得將?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í)行后將會看到以下日志信息,展示了整個數據庫請求的詳細執(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
?上下文變量沒有層層傳遞下去,那么嵌套事務將會失敗,我們來看一個錯誤的例子:
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í)行后,您將會看到以下日志內容:
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')
? 沒有事務?ID
?打印,表示沒有使用到事務,那么該操作將會被真正提交到數據庫執(zhí)行,并不能被回滾。
開發(fā)者也可以靈活使用?Transaction Save Point
?特性,并實現(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
?日志,那么將會看到以下日志信息,展示了整個數據庫請求的詳細執(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í)行后查詢數據庫結果:
mysql> select * from `user`;
+----+------+
| id | name |
+----+------+
| 1 | john |
+----+------+
1 row in set (0.00 sec)
可以看到,通過在第一個?Insert
?操作后保存了一個?SavePoint
?名稱?MyPoint
?,隨后的幾次操作都通過?RollbackTo
?方法被回滾掉了,因此只有第一次?Insert
?操作被成功提交執(zhí)行。
為了簡化示例,我們還是使用用戶模塊相關的示例,例如用戶注冊,通過事務操作保存用戶基本信息(?user
?)、詳細信息(?user_detail
?)兩個表,任一個表操作失敗整個注冊操作都將失敗。為展示嵌套事務效果,我們將用戶基本信息管理和用戶詳細信息管理劃分為了兩個?dao
?對象。
假如我們的項目按照?goframe
?標準項目工程化分為三層?api-service-dao
?,那么我們的嵌套事務操作可能是這樣的。
// 用戶注冊HTTP接口
func (*cUser) Signup(r *ghttp.Request) {
// ....
service.User().Signup(r.Context(), userServiceSignupReq)
// ...
}
承接?HTTP
?請求,并且將?Context
?上下文邊變量傳遞給后續(xù)的流程。
// 用戶注冊業(yè)務邏輯處理
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
})
// ...
}
可以看到,內部的?user
?表和?user_detail
?表使用了嵌套事務來統(tǒng)一執(zhí)行事務操作。注意在閉包內部需要通過?Ctx
?方法將上下文變量傳遞給下一層級。假如在閉包中存在對其他?service
?對象的調用,那么也需要將?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
?層的代碼由?goframe cli
?工具全自動化生成及維護即可。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: