gorm事务体验
简介
表
CREATE TABLE `accounts` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
`money` decimal(30,2) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

没有事务
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
type Account struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"type:varchar(10);not null"`
Money float64 `gorm:"type:decimal(30,2);not null"`
}
func main() {
// 连接对应的数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
"root", "root", "127.0.0.1", 3306, "test")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 使用用彩色打印
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
a := 1 - 1
// 模拟张三给李四转100
account := Account{ID: 1, Name: "张三", Money: 900}
account1 := Account{ID: 2, Name: "李四", Money: 1100}
db.Save(&account)
if true {
// 手动制造一个错误
_ = 1 / a
}
db.Save(&account1)
}

在代码中手动制造了一个异常,正常情况下应该转账失败,两个人的余额都还是1000,但是由于没有使用事务,张三已经发生了扣款,但是李四却没有收到钱,这在真实的场景中是绝对不能接受的.
事务
package main
import (
"errors"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
type Account struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"type:varchar(10);not null"`
Money float64 `gorm:"type:decimal(30,2);not null"`
}
func main() {
// 连接对应的数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
"root", "root", "127.0.0.1", 3306, "test")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 使用用彩色打印
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
db.Transaction(func(tx *gorm.DB) error {
// 模拟张三给李四转100
account := Account{ID: 1, Name: "张三", Money: 800}
account1 := Account{ID: 2, Name: "李四", Money: 1100}
tx.Save(&account)
if true {
return errors.New("手动制造一个错误")
}
tx.Save(&account1)
return nil
})
}

使用事务以后就是正常的,出现错误会回滚,返回任何错误都会回滚事务
嵌套事务
package main
import (
"errors"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
type Account struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"type:varchar(10);not null"`
Money float64 `gorm:"type:decimal(30,2);not null"`
}
func main() {
// 连接对应的数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
"root", "root", "127.0.0.1", 3306, "test")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 使用用彩色打印
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
db.Transaction(func(tx *gorm.DB) error {
// 模拟张三给李四转100
account := Account{ID: 1, Name: "张三", Money: 800}
account1 := Account{ID: 2, Name: "李四", Money: 1100}
tx.Save(&account)
tx.Transaction(func(tx *gorm.DB) error {
// 嵌套事务中模仿王五给赵六转100
account2 := Account{ID: 3, Name: "王五", Money: 900}
account3 := Account{ID: 4, Name: "赵六", Money: 1100}
tx.Save(account2)
if true {
return errors.New("手动制造一个错误")
}
tx.Save(account3)
return nil
})
tx.Save(&account1)
return nil
})
}

这次错误是发生再嵌套事务中,张三给李四转账是正常完成的,但是王五给赵六转账是出现回滚的
手动事务
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
type Account struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"type:varchar(10);not null"`
Money float64 `gorm:"type:decimal(30,2);not null"`
}
func main() {
// 连接对应的数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
"root", "root", "127.0.0.1", 3306, "test")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 使用用彩色打印
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 模拟张三给李四转100
// 开启事务
tx := db.Begin()
account := Account{ID: 1, Name: "张三", Money: 700}
account1 := Account{ID: 2, Name: "李四", Money: 1200}
if result := tx.Save(&account); result.Error != nil {
// 出现错误回滚
tx.Rollback()
}
if result := tx.Save(&account1); result.Error != nil {
// 出现错误回滚
tx.Rollback()
}
// 提交事务
tx.Commit()
}

中间没有出现任何错误,提交事务转账成功.手动事务对error进行判断,如果error不为nil就直接使用tx.Rollback()回滚事务就好了.
SavePoint、RollbackTo
GORM 提供了 SavePoint、Rollbackto 方法,来提供保存点以及回滚至保存点功能
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
type Account struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"type:varchar(10);not null"`
Money float64 `gorm:"type:decimal(30,2);not null"`
}
func main() {
// 连接对应的数据库
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
"root", "root", "127.0.0.1", 3306, "test")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 使用用彩色打印
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
// 模拟张三给李四转100
// 模拟王五给赵六转100
account := Account{ID: 1, Name: "张三", Money: 600}
account1 := Account{ID: 2, Name: "李四", Money: 1300}
account2 := Account{ID: 3, Name: "王五", Money: 900}
account3 := Account{ID: 4, Name: "赵六", Money: 1100}
tx := db.Begin()
tx.Save(&account)
tx.Save(&account1)
tx.SavePoint("test")
tx.Save(&account2)
tx.Save(&account3)
if true {
// 在这里回滚,只完成张三给李四转账
tx.RollbackTo("test")
}
tx.Commit()
}

使用tx.RollbackTo(“test”)回滚到tx.SavePoint(“test”)之前,只完成张三给李四转账;未完成王五给赵六的转账
注意



再官方文档中,作者秉承着重要的事情说三遍的原则,事务一旦开始就应该使用 tx 处理数据,这一点需要特别注意,如果使用事务之前的db处理事务,出现错误是不会回滚的