事务和Hook
1. 会话Session
为了避免共用db导致的一些问题,gorm提供了会话模式,通过新建session的形式,将db的操作分离,互不影响。
创建session的时候,有一些配置:
// Session 配置
type Session struct {
DryRun bool //生成 SQL 但不执行
PrepareStmt bool //预编译模式
NewDB bool //新db 不带之前的条件
Initialized bool //初始化新的db
SkipHooks bool //跳过钩子
SkipDefaultTransaction bool //禁用默认事务
DisableNestedTransaction bool //禁用嵌套事务
AllowGlobalUpdate bool //允许不带条件的更新
FullSaveAssociations bool //允许更新关联数据
QueryFields bool //select(字段)
Context context.Context
Logger logger.Interface
NowFunc func() time.Time //允许改变 GORM 获取当前时间的实现
CreateBatchSize int
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
比如说可以禁用默认的事务,从而提供性能,官方说大致能提升30%左右:
// 持续会话模式
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
1
2
3
4
5
2
3
4
5
比如使用PreparedStmt
在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率
// 会话模式
tx := db.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
// returns prepared statements manager
stmtManger, ok := tx.ConnPool.(*PreparedStmtDB)
// 关闭 *当前会话* 的预编译模式
stmtManger.Close()
// 为 *当前会话* 预编译 SQL
stmtManger.PreparedSQL // => []string{}
// 为当前数据库连接池的(所有会话)开启预编译模式
stmtManger.Stmts // map[string]*sql.Stmt
for sql, stmt := range stmtManger.Stmts {
sql // 预编译 SQL
stmt // 预编译模式
stmt.Close() // 关闭预编译模式
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
还有,gorm的db默认是协程安全的,如果使用初始化参数,则db不在协程安全:
tx := db.Session(&gorm.Session{Initialized: true})
1
比如context:
timeoutCtx, _ := context.WithTimeout(context.Background(), time.Second)
tx := db.Session(&Session{Context: timeoutCtx})
tx.First(&user) // 带有 context timeoutCtx 的查询操作
tx.Model(&user).Update("role", "admin") // 带有 context timeoutCtx 的更新操作
1
2
3
4
5
6
2
3
4
5
6
2. 事务
2.1 自动事务
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
2.2 嵌套事务
GORM 支持嵌套事务,您可以回滚较大事务内执行的一部分操作,例如:
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback user2") // Rollback user2
})
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user3)
return nil
})
return nil
})
// Commit user1, user3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2.3 手动事务
// 开始事务
tx := db.Begin()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)
// ...
// 遇到错误时回滚事务
tx.Rollback()
// 否则,提交事务
tx.Commit()
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
比如
// 开启事务
tx := db.Begin()
//在事务中执行数据库操作,使用的是tx变量,不是db。
//库存减一
//等价于: UPDATE `goods` SET `stock` = stock - 1 WHERE `goods`.`id` = '2' and stock > 0
//RowsAffected用于返回sql执行后影响的行数
rowsAffected := tx.Model(&goods).Where("stock > 0").Update("stock", gorm.Expr("stock - 1")).RowsAffected
if rowsAffected == 0 {
//如果更新库存操作,返回影响行数为0,说明没有库存了,结束下单流程
//这里回滚作用不大,因为前面没成功执行什么数据库更新操作,也没什么数据需要回滚。
//这里就是举个例子,事务中可以执行多个sql语句,错误了可以回滚事务
tx.Rollback()
return
}
err := tx.Create(保存订单).Error
//保存订单失败,则回滚事务
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2.4 保存点
GORM 提供了 SavePoint
、Rollbackto
方法,来提供保存点以及回滚至保存点功能,例如:
tx := db.Begin()
tx.Create(&user1)
tx.SavePoint("sp1")
tx.Create(&user2)
tx.RollbackTo("sp1") // Rollback user2
tx.Commit() // Commit user1
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
3. Hook
Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。
如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。
钩子方法的函数签名应该是 func(*gorm.DB) error
3.1 创建
创建时可用的 hook
// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
u.UUID = uuid.New()
if !u.IsValid() {
err = errors.New("can't save invalid data")
}
return
}
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if u.ID == 1 {
tx.Model(u).Update("role", "admin")
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
if !u.IsValid() {
return errors.New("rollback invalid user")
}
return nil
}
1
2
3
4
5
6
2
3
4
5
6
3.2 更新
更新时可用的 hook
// 开始事务
BeforeSave
BeforeUpdate
// 关联前的 save
// 更新 db
// 关联后的 save
AfterUpdate
AfterSave
// 提交或回滚事务
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
代码示例:
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
if u.readonly() {
err = errors.New("read only user")
}
return
}
// 在同一个事务中更新数据
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
if u.Confirmed {
tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
}
return
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
3.3 删除
删除时可用的 hook
// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务
1
2
3
4
5
2
3
4
5
代码示例:
// 在同一个事务中更新数据
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
if u.Confirmed {
tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
}
return
}
1
2
3
4
5
6
7
2
3
4
5
6
7
3.4 查询
查询时可用的 hook
// 从 db 中加载数据
// Preloading (eager loading)
AfterFind
1
2
3
2
3
代码示例:
func (u *User) AfterFind(tx *gorm.DB) (err error) {
if u.MemberShip == "" {
u.MemberShip = "user"
}
return
}
1
2
3
4
5
6
2
3
4
5
6