一. 初始化
- gorm: go中一个比较方便操作数据库的三方库
- 初始化连接
- 只给go get 命令,拉取mysql驱动与gorm包
- 读取数据库配置信息,调用grom下open()函数创建数据库连接
package mysqldatabase
import (
"lmcd_siteserver/config"
"lmcd_siteserver/log"
"os"
"time"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
var (
//DB 数据库连接实例
DB = New()
)
//New 新建实例
func New() *gorm.DB {
log.TraceLog("InitDB", "Start Connect MySql")
driver := "mysql"
userName, err := config.Conf.GetValue(driver, "username")
if err != nil {
log.ErrorLog("InitDB", "Load Mysql Error: read config value failed mysql username ")
return nil
}
password, err := config.Conf.GetValue(driver, "password")
if err != nil {
log.ErrorLog("InitDB", "Load Mysql Error: read config value failed mysql password ")
return nil
}
database, err := config.Conf.GetValue(driver, "database")
if err != nil {
log.ErrorLog("InitDB", "Load Mysql Error: read config value failed mysql password ")
return nil
}
url, err := config.Conf.GetValue(driver, "url")
if err != nil {
log.ErrorLog("InitDB", "Load Mysql Error: read config value failed mysql url ")
return nil
}
connect := userName + ":" + password + "@tcp(" + url + ":3306)/" + database + "?charset=utf8&parseTime=True&loc=Local"
DB, err := gorm.Open(driver, connect)
if err != nil {
log.ErrorLog("InitDB", "open mysql failed %s ", connect)
os.Exit(0)
}
log.TraceLog("InitDB", "connect mysql OK")
DB.DB().SetConnMaxLifetime(time.Second * 5)
DB.DB().SetMaxIdleConns(8)
DB.DB().SetMaxOpenConns(151)
migrateDB(DB)
DB.LogMode(true)
return DB
}
// 自动更新表结构
func migrateDB(db *gorm.DB) {
//db.AutoMigrate(&DBTicketPriceActivity{})
}
二. 增删改查示例
- 增删改查接口示例
- 编写对应指定表的结构体
- 针对指定表编写增删改查接口
//TODO 注意当前使用的是"github.com/jinzhu/gorm"这个版本的gorm
import (
"encoding/json"
"lmcd_siteserver/lmerror"
sensitive "lmcd_siteserver/util/aes"
"time"
)
type DBSiteApplication struct {
ApplicationId int `gorm:"primary_key;column:application_id" json:"applicationId"`
SiteCode string `gorm:"not null;column:site_code;type:varchar(125);comment:'场所code';default:''" json:"siteCode"`
ManagementType int `gorm:"not null;type:int(3);column:management_type;comment:'经营类型:1网约房';default:1" json:"managementType"`
City string `gorm:"not null;column:city;type:varchar(20);comment:'城市';default:''" json:"city"`
Community string `gorm:"not null;column:community;type:varchar(20);comment:'社区';default:''" json:"community"`
SiteAddress string `gorm:"not null;column:site_address;type:varchar(256);comment:'场所详细地址';default:''" json:"siteAddress"`
Name string `gorm:"not null;column:name;type:varchar(20);comment:'申请者姓名';default:''" json:"name"`
CardNumMd string `gorm:"not null;column:card_num_md;type:varchar(20);comment:'证件号(脱敏)';default:''" json:"cardNum"`
CardNumEn string `gorm:"not null;column:card_num_en;type:varchar(128);comment:'证件号(完全加密)';default:''" json:"cardNumEn"`
MobileMd string `gorm:"not null;column:mobile_md;type:varchar(20);comment:'手机号(脱敏)';default:''" json:"mobile"`
MobileEn string `gorm:"not null;column:mobile_en;type:varchar(128);comment:'手机号(完全加密)';default:''" json:"mobileEn"`
CardAddress string `gorm:"not null;column:card_address;type:varchar(60);comment:'申请人证件地址';default:''" json:"cardAddress"`
CheckerId string `gorm:"not null;column:checker_id;type:varchar(20);comment:'审核员id';default:''" json:"checkerId"`
CheckerDesc string `gorm:"not null;column:checker_desc;type:varchar(128);comment:'审核描述';default:''" json:"checkerDesc"`
Pic string `gorm:"not null;column:pic;type:varchar(128);comment:'证件照';default:''" json:"pic"`
LicensePic string `gorm:"not null;column:license_pic;type:varchar(128);comment:'营业执照';default:''" json:"licensePic"`
SubmitType int `gorm:"not null;type:int(3);column:submit_type;comment:'提交发起方:1用户端,2民警端';default:1" json:"submitType"`
Status int `gorm:"not null;type:int(3);column:status;comment:'审核状态:1待审核, 2审核通过, 3审核失败';default:1" json:"status"`
CreateDate time.Time `gorm:"not null;column:create_date;type:timestamp;comment:'创建时间';default:Now()" json:"createDate"`
UpdateDate time.Time `gorm:"not null;column:update_date;type:timestamp;comment:'更新时间';default:Now()" json:"updateDate"`
}
//TableName 数据库表名
func (c *DBSiteApplication) TableName() string {
return "site_application"
}
//TODO 也可以去掉下面方法中的db gorm.DB这个入参,将数据库连接做成全局变量,直接在方法内获取即可,这样调用这些方法时就可以少传一个值了
//Add 添加记录
func (c *DBSiteApplication) Add(db *gorm.DB) error {
return db.Create(c).Error
}
//Update 只修改带参结构
func (c *DBSiteApplication) Update(db *gorm.DB) error {
return db.Model(c).Update(c).Error
}
//UpdateSave 全修改
func (c *DBSiteApplication) UpdateSave(db *gorm.DB) error {
return db.Model(c).Save(c).Error
}
//根据结构体查询并映射
func (c *DBSiteApplication) Search(db *gorm.DB) error {
return db.Model(c).Where(c).First(c).Error
}
func SelectSiteApplicationById(applicationId int,db *gorm.DB) (*DBSiteApplication, error) {
var siteApplication DBSiteApplication
//条件查询
err := db.Where("application_id = ? ", applicationId).Find(&siteApplication).Error
if err != nil {
return nil, err
}
return &siteApplication, nil
}
//等同于
func SelectById(id int) (*DBSiteApplication, error) {
var siteApplication DBSiteApplication
//条件查询
err := db.First(&siteApplication, id).Error
if err != nil {
return nil, err
}
return &siteApplication, nil
}
func SelectSiteApplicationByCode(siteCode string, managementType, status int,db *gorm.DB) (*DBSiteApplication, error) {
var siteApplication DBSiteApplication
//条件查询
err := db.Where("site_code = ? and management_type = ? and status = ?", siteCode, managementType, status).Find(&siteApplication).Error
if err != nil {
return nil, err
}
return &siteApplication, nil
}
//支持SiteAddress模糊
func (c *DBSiteApplication) SelectByConditionsValid(db *gorm.DB) ([]DBSiteApplication, error) {
var applicationList []DBSiteApplication
if 4 == c.Status {
db = db.Where("status= 1").Or("status =3")
c.Status = 0
}
if 0 < len(c.SiteAddress) {
db = db.Where("site_address LIKE ?", c.SiteAddress+"%")
c.SiteAddress = ""
}
db = db.Where(c)
err := db.Find(&applicationList).Error
if err != nil {
return nil, err
}
return applicationList, nil
}
func (c *DBSiteApplication) SelectByPageValid(pageNo, pageSize int,db *gorm.DB) ([]DBSiteApplication, int, error) {
var applicationList []DBSiteApplication
var count int
if 0 < len(c.SiteAddress) {
db = db.Where("site_address LIKE ?", c.SiteAddress+"%")
c.SiteAddress = ""
}
/*if 0 == c.Status {
db = db.Not("status", 3)
}*/
if 4 == c.Status {
db = db.Where("status= 1").Or("status =3")
c.Status = 0
}
db = db.Model(&DBSiteApplication{}).Where(c)
limit := pageSize
offset := pageSize * (pageNo - 1)
//注意Limit 方法和 Offset 方法必须在 Find 方法之前调用,否则会出现错误。
err:= db.Count(&count).Limit(limit).Offset(offset).Find(&applicationList).Error
if err!= nil {
return nil, 0, err
}
return applicationList, count, nil
}
- 执行接口
func SiteApplicationQueryPage(msgid string, request req.QueryPage, db gorm.DB) (*entity.PageResp, error) {
siteApplication := mysqldatabase.DBSiteApplication{
ApplicationId: request.ApplicationId,
ManagementType: request.ManagementType,
SiteCode: request.SiteCode,
Status: request.Status,
Community: request.Community,
SiteAddress: request.SiteAddress,
Name: request.Name,
CardNumEn: request.GetCardNumEn(),
MobileEn: request.GetMobileEn(),
CheckerId: request.CheckerId,
}
list, count, err := siteApplication.SelectByPageValid(request.PageNo, request.PageSize)
if nil != err {
return nil, err
}
pageTotal := 0
if 0 != count {
pageTotal = (count + request.PageSize) / request.PageSize
}
return &entity.PageResp{
Total: count,
PageTotal: pageTotal,
PageNo: request.PageNo,
List: ApplicationPoToVoList(list),
}, nil
}
结构体tag解释
标签名 说明
column 指定 db 列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null、size, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer 指定将数据序列化或反序列化到数据库中的序列化器, 例如: serializer:json/gob/unixtime
size 定义列数据类型的大小或长度,例如 size: 256
primaryKey 将列定义为主键
unique 将列定义为唯一键
default 定义列的默认值
precision 指定列的精度
scale 指定列大小
not null 指定列为 NOT NULL
autoIncrement 指定列为自动增长
autoIncrementIncrement 自动步长,控制连续记录之间的间隔
embedded 嵌套字段
embeddedPrefix 嵌入字段的列名前缀
autoCreateTime 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex 与 index 相同,但创建的是唯一索引
check 创建检查约束,例如 check:age > 13,查看 约束 获取详情
<- 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
-> 设置字段读的权限,->:false 无读权限
- 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment 迁移时为字段添加注释
Save与Update区别
- Create 用于创建新的记录并向数据库中插入一条新数据
- Save 主要用于保存数据,如果记录不存在,则会创建新的记录;如果记录存在则会更新该记录的所有字段
- Update 只会更新指定字段,不会更新空值字段,并且需要手动指定更新条件
GORM中的钩子
BeforeSave:保存之前调用。执行 Create、Save 和 Update 时都会触发该钩子函数。
AfterSave:保存之后调用。执行 Create、Save 和 Update 时都会触发该钩子函数。
BeforeCreate:创建之前调用。只有执行 Create 方法时才会触发该钩子函数。
AfterCreate:创建之后调用。只有执行 Create 方法时才会触发该钩子函数。
BeforeUpdate:更新之前调用。只有执行 Save 和 Update 方法时才会触发该钩子函数。
AfterUpdate:更新之后调用。只有执行 Save 和 Update 方法时才会触发该钩子函数
BeforeDelete:删除之前调用。只有执行 Delete 方法时才会触发该钩子函数。
AfterDelete:删除之后调用。只有执行 Delete 方法时才会触发该钩子函数。
GORM Context支持
- gorm 中提供了WithContext 方法用于将上下文Context与数据库操作相关联,将一个 context.Context 对象绑定到一个 *gorm.DB 对象上,在数据库操作期间,这个上下文对象将被传递到 gorm 库的各个函数里,可以进行一些控制或者取消数据库操作的行为,使用示例
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
user := User{Name: "Tom", Age: 18}
result := db.WithContext(ctx).Create(&user)
if err := result.Error; err != nil {
if errors.Is(err, context.DeadlineExceeded) {
// 超时取消,进行处理
} else if errors.Is(err, context.Canceled) {
// 手动取消,进行处理
} else {
// 其他错误,进行处理
}
}
GORM 与锁
- gorm 中,提供了多种类型的锁,包括行级锁(Row-level Lock)、表级锁(Table-level Lock)、共享锁(Shared Lock)和排它锁(Exclusive Lock)等。其中,最常用的是共享锁和排它锁
- 共享锁(Shared Lock):允许多个事务同时读取同一资源,但不允许进行写操作,可防止数据读取出错。在 gorm 中,可以使用 db.Set(“gorm:query_option”, “FOR SHARE”) 语句来获取共享锁
var user User
// 获取共享锁
if err := db.Where("name = ?", "Tom").Set("gorm:query_option", "FOR SHARE").First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
- 排它锁(Exclusive Lock):只允许一个事务对一个资源进行读或写操作,可防止数据读取和写入出错。在 gorm 中,可以使用 db.Set(“gorm:query_option”, “FOR UPDATE”) 语句来获取排它锁
var user User
// 获取排它锁
if err := db.Where("name = ?", "Tom").Set("gorm:query_option", "FOR UPDATE").First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
- gorm 还提供了一些其他的锁类型,例如 NOWAIT 、 SKIP LOCKED 、 SHARE ROW EXCLUSIVE 等
GORM的预加载Preload与Joins
- gorm中提供了预加载功能Preload(),查看一对一中的使用示例
- 预加载也分为懒汉式与饿汉式两种,可以通过设置 AutoPreload 来开启 Lazy Loading 功能(可能会导致数据冗余和查询性能下降等问题)
db = db.Set("gorm:auto_preload", true)
- 还有一种是选择性预加载可以通过 Select 方法来指定需要预加载的内容
var user User
if err := db.Preload("Orders").Select("name, age").Where("name = ?", "Tom").First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrUserNotFound
}
return nil, err
}
- Preload 在一个单独查询中加载关联数据。Joins 预加载会对每个表执行单独的查询,而不是一次性将所有相关表的数据全部加载进来,注意的Joins 预加载会对每个表执行单独的查询,如果需要同时加载多个表中的大量数据,可能会导致查询性能下降, 支持 Inner Join、Left Join、Right Join、Cross Join 等
// 查询所有用户及对应的订单信息(内连接)
var users []User
if err := db.Joins("INNER JOIN orders ON users.id = orders.user_id").Find(&users).Error; err != nil {
return nil, err
}
// 查询所有用户及对应的订单信息(左连接)
var users []User
if err := db.Joins("LEFT JOIN orders ON users.id = orders.user_id").Find(&users).Error; err != nil {
return nil, err
}
//......
查询时优雅的处理动态条件
- gorm 支持使用结构体、map 和字符串三种方式来构建查询条件
- 字符串方式上方有就不再进行示例
- 结构体查询条件示例
// 定义查询条件结构体
type UserQuery struct {
Name string `gorm:"column:name;like"`
Age int `gorm:"column:age;gt"`
}
// 创建查询条件对象
query := UserQuery{Name: "%john%", Age: 18}
// 执行查询
var users []User
err := db.Where(&query).Find(&users).Error
if err != nil {
// 处理错误
}
- map查询条件示例
// 定义查询条件的 map
query := make(map[string]interface{})
query["name LIKE ?"] = "%john%"
query["age > ?"] = 18
// 执行查询
var users []User
err := db.Where(query).Find(&users).Error
if err != nil {
// 处理错误
}
- gorm 内置了 Squirrel 库作为查询构造器,可以看为一种更优雅的方式
import (
"github.com/Masterminds/squirrel"
)
// 定义查询条件
var whereBuilder squirrel.And
// 构建条件查询语句
if name != "" {
whereBuilder = append(whereBuilder, squirrel.Expr("name LIKE ?", "%"+name+"%"))
}
if age > 0 {
whereBuilder = append(whereBuilder, squirrel.Expr("age > ?", age))
}
// 执行查询
query := db.Model(&User{})
query = query.Where(whereBuilder)
var users []User
err := query.Find(&users).Error
if err != nil {
// 处理错误
}
分页
- gorm 提供了一些内置方法来实现分页查询,最常用的是 Limit 和 Offset 方法
// 计算起始位置
page := 1 // 第一页
pageSize := 10 // 每页10条记录
offset := (page - 1) * pageSize
// 执行查询
var users []User
err := db.Limit(pageSize).Offset(offset).Find(&users).Error
if err != nil {
// 处理错误
}
- gorm v2.x 版本,可以使用 Limit 和 Offset 方法的链式调用来更加优雅地实现分页查询
page := 1
pageSize := 10
// 执行查询
var users []User
err := db.Limit(pageSize).Offset((page - 1) * pageSize).Find(&users).Error
if err != nil {
// 处理错误
}
- gorm 还提供了一种更加优雅的方式Paginate 方法(gorm v2.x 版本支持)来实现分页查询,Paginate 方法是 gorm 的一个扩展包 gorm.io/plugin 提供的特性,需要先导入该包
import (
"gorm.io/plugin"
)
// 执行分页查询
page := 1
pageSize := 10
var users []User
err := db.Scopes(
plugin.Paginate(page, pageSize),
).Find(&users).Error
if err != nil {
// 处理错误
}
gorm.io/plugin扩展包
- gorm v2.x 版本提供了一个扩展包 gorm.io/plugin,其中包含了许多实用的工具和插件,以下是其中一些常用的扩展工具:
- Audit 插件:记录数据变更历史,包括创建时间、更新时间、删除时间以及操作者。
- SoftDelete 插件:在删除数据时进行软删除,即将删除标记设置为 true 而不是从数据库中直接删除数据。
- Preload 插件:使用预加载机制优化关联查询,避免 N + 1 查询问题。
- Scopes 插件:使用作用域模式对查询条件进行封装和复用。
- UUID 类型支持:在类型映射中增加了对 UUID 类型的支持。
- Paginate 插件:对分页查询进行封装,使得代码更加简洁和易读。
- 除了上述扩展工具之外,还有一些其他的实用扩展工具,如 Debug、Import、Interceptors 等。需要根据实际需求进行选择和使用。
三. 问题
GORM 如何实现一对多和多对多关系的映射
关联标签
1. 一对一
- gorm中BelongsTo 或 HasOne 定义一对一关系,HasMany 定义一对多关系,多对多关系可以使用 ManyToMany 方法或 HasMany 方法来定义
- 一对一BelongsTo 或 HasOne(实际开发中使用其中一种即可,参考User 与 Profile )
//在 Book 模型中使用 BelongsTo 方法定义了 Author 字段,用于表示一本书只有一个作者。
//同时,在 Author 模型中使用 HasOne 方法定义了 Book 字段,用于表示一个作者只写了一本书
type Author struct {
gorm.Model
Name string
// HasOne 定义一对一关系
Book Book // 一个作者只有一个书
}
type Book struct {
gorm.Model
Title string
// BelongsTo 定义一对一关系
AuthorID uint // 一本书只有一个作者
Author Author
}
//示例2
type User struct {
gorm.Model
Name string
Email string
Profile Profile // 定义与 Profile 结构体的关联关系
}
type Profile struct {
gorm.Model
UserID uint // 关联的 User 模型的主键 ID
Age uint8
Address string
Education string
}
//调用Preload 方法,传入了 "Profile" 字符串作为参数,表示预加载 User 模型中的 Profile 字段。
//这样,查询到的 User 对象就已经包含了与之关联的 Profile 对象,可以直接访问
var user User
result := db.Preload("Profile").First(&user, id)
if result.Error != nil {
// 处理错误
}
- Preload 方法预加载注意事项:应该尽量避免传入一个包含多层嵌套关系的字符串参数(如 “Profile.Address”),会导致预加载的效果不佳,甚至可能出现错误。如果需要预加载多个关联模型,可以在调用 Preload 方法时多次传入参数,例如
var user User
result := db.Preload("Profile").Preload("Orders").First(&user, id)
if result.Error != nil {
// 处理错误
}
2. 一对多
- 一对多HasMany
type User struct {
gorm.Model
Name string
Email string
// HasMany 定义一对多关系
Articles []Article // 定义与 Article 结构体的关联关系
}
type Article struct {
gorm.Model
UserID uint // 关联的 User 模型的主键 ID
Title string
Content string
}
//使用Preload()进行预加载
var user User
result := db.Preload("Articles").First(&user, id)
if result.Error != nil {
// 处理错误
}
- 如果要进行自定义的条件过滤和排序,可以使用 GORM 提供的关联查询方法。例如,以下示例展示了如何按照 Article 模型中的 created_at 字段对 User 模型的 Articles 字段进行倒序排序:
var user User
result := db.Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC")
}).First(&user, id)
if result.Error != nil {
// 处理错误
}
- 需要注意的是,在一对多关联中,如果要通过关联字段进行查询和过滤,可以使用 GORM 提供的 Association 方法和 Where 方法
// User 模型对应的表应该包含一个外键,指向 Address 表中的 UserID 字段
// 使用 BelongsTo 和 HasMany 方法进行关联
func (u *User) Articles() []Address {
var articles[]Article
DB.Model(&u).Association("Addresses").Find(&articles)
return articles
}
func (a *Article) User() User {
var user User
DB.Model(&a).Association("User").Find(&user)
return user
}
3. 多对多
- 对于多对多关系,可以使用 GORM 的 ManyToMany 方法实现映射
//在 User 和 Role 模型中使用 ManyToMany 方法定义了 Roles 和 Users 字段,
//用于表示一个用户可以拥有多个角色,一个角色也可以被多个用户拥有。
//需要在定义中指定中间表的名称和格式
type User struct {
gorm.Model
Name string
// ManyToMany 定义多对多关系
Roles []Role `gorm:"many2many:user_roles;"`
}
type Role struct {
gorm.Model
Name string
// ManyToMany 定义多对多关系
Users []User `gorm:"many2many:user_roles;"`
}
GORM 进行数据库查询时如何避免 N+1 查询的问题
- N+1 查询问题指的是在查询关联表时,如果使用了嵌套循环进行查询,就会产生大量的 SQL 查询。为了避免这个问题,可以使用 GORM 的 Preload 方法预先加载关联数据
// 查询 Users 以及它们的 Addresses
//这个查询会一次性加载所有 User 和 Address 数据,避免了 N+1 查询问题
var users []User
DB.Preload("Addresses").Find(&users)
GORM 的 Preload 方法和 Joins 方法有什么区别
- Preload 方法是在查询时预加载关联数据,而 Joins 方法是通过 SQL JOIN 语句连接多个表查询数据。Preload 方法适用于关联表较少、数据量不大的情况;而 Joins 方法适用于关联表较多、数据量较大的情况
如何在 GORM 中使用原生 SQL 查询
- 可以使用 Raw 方法来执行原生 SQL 查询。Raw 方法接受一个 SQL 查询字符串和可选的参数列表,并返回一个 *gorm.DB 对象,可以使用该对象进行进一步的查询操作
import "gorm.io/gorm"
// ...
var users []User
result := db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
if result.Error != nil {
// 处理错误
}
- 还可以使用 Exec 方法来执行不需要返回值的 SQL 查询,如插入、更新或删除数据
result := db.Exec("DELETE FROM users WHERE age < ?", 18)
if result.Error != nil {
// 处理错误
}
GORM与事物
- gorm事物基本使用示例
import (
"gorm.io/gorm"
"gorm.io/driver/mysql"
)
type User struct {
gorm.Model
Name string
Age int
}
func main() {
//1.获取数据库连接
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
//2.开启事物
tx := db.Begin()
//3.1在事务中插入一条记录
err = tx.Create(&User{Name: "jinzhu", Age: 18}).Error
if err != nil {
//回滚
tx.Rollback() // 发生错误时回滚事务
panic(err)
}
//3.2在事务中更新一条记录
err = tx.Model(&User{}).Where("name = ?", "jinzhu").Update("age", 20).Error
if err != nil {
tx.Rollback() // 发生错误时回滚事务
panic(err)
}
//4.提交事物
err = tx.Commit().Error
if err != nil {
tx.Rollback() // 发生错误时回滚事务
panic(err)
}
}
1. 如何优雅的处理事物
- 像java框架中使用@Transactional可以使事物代码与业务代码解耦,go中怎么操作?
- 以gin整合gorm为例,定义事物操作的中间件,将业务与事物操作解耦
//1.定义事物操作中间件,开启事物后,将事物句柄tx存入上下文中
func TransactionMiddleware(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context){
// 开启事务
tx := db.Begin()
// 将事务绑定到请求上下文
c.Set("tx", tx)
// 执行下一个中间件或路由
c.Next()
// 根据响应状态,提交或回滚事务
if c.Writer.Status() >= 500 {
tx.Rollback()
} else {
tx.Commit()
}
}
}
//2.注册带有事物操作的业务路由时增加该中间件
router := gin.New()
router.Use(TransactionMiddleware(db))
//3.业务逻辑中直接在上下文中获取事物句柄操作数据库
tx := c.MustGet("tx").(*gorm.DB)
tx.Where(...).First(&user)
2. Transaction嵌套事物
- Transaction使用示例
- db.Transaction()方法接受一个函数作为参数,这个函数也接受一个*gorm.DB类型的参数,表示当前的事务对象。这个函数需要返回一个error类型的值,表示事务是否成功或失败。
- db.Transaction()方法在内部会调用db.Begin()方法来开启一个新的事务,并将事务对象作为参数传递给函数。
- 函数在内部可以使用事务对象来执行一些数据库操作,比如增删改查等。如果所有的操作都成功,则返回nil表示事务提交;如果有任何操作失败,则返回错误表示事务回滚。
- db.Transaction()方法在接收到函数的返回值后,会根据返回值的类型来决定是调用tx.Commit()方法来提交事务,还是调用tx.Rollback()方法来回滚事务。
- db.Transaction()方法最终也会返回一个error类型的值,表示整个事务模板的执行结果
func (baseMenuService *BaseMenuService) UpdateBaseMenu2(menu system.SysBaseMenu) (err error) {
//1.创建数据库句柄
db, _ := gorm.Open(mysql.New(mysql.Config{DSN: "root:123456@tcp(127.0.0.1:3306)/gormDB?charset=utf8mb4&parseTime=True&loc=Local"}), &gorm.Config{})
//2.通过Transaction()执行带事物操作
err = db.Transaction(func(tx *gorm.DB) error {
//2.1查询id为menu.ID数据赋值给oldMenu结构体变量
var oldMenu *system.SysBaseMenu
db := tx.Where("id = ?", menu.ID).Find(oldMenu)
if oldMenu == nil {
return errors.New("返回异常")
}
//2.2执行删除操作,删除sys_base_menu_id为menu.ID数据
//Unscoped()是gorm中用来去除一下默认设置的函数
txErr := tx.Unscoped().Delete(&system.SysBaseMenuParameter{}, "sys_base_menu_id = ?", menu.ID).Error
if txErr != nil {
//如果error不为空,直接返回,返回error不为空时会触发事物的回滚
return txErr
}
//2.3执行删除动作
txErr = tx.Unscoped().Delete(&system.SysBaseMenuBtn{}, "sys_base_menu_id = ?", menu.ID).Error
if txErr != nil {
return txErr
}
if len(menu.Parameters) > 0 {
//2.4执行插入动作
txErr = tx.Create(&menu.Parameters).Error
if txErr != nil {
return txErr
}
}
if len(menu.MenuBtn) > 0 {
//2.5执行插入动作
txErr = tx.Create(&menu.MenuBtn).Error
if txErr != nil {
return txErr
}
}
//执行更新动作
upDateMap := make(map[string]interface{})
upDateMap["keep_alive"] = menu.KeepAlive
txErr = db.Updates(upDateMap).Error
if txErr != nil {
return txErr
}
return nil
})
return err
}
- 嵌套事物的底层原理:
- 使用db.Transaction(func(tx gorm.DB) error {…})方法来实现嵌套事物,该方法需接收一个执行数据库相关操作的回调函数,gorm会调用db.Begin()方法来开启一个新的gorm.DB实例,并将其作为参数传递给回调函数
- 在回调函数中,也就是Transaction内部执行数据库操作的函数,可以再次调用tx.Transaction(func(tx2 gorm.DB) error {…})方法来执行一个内部的子事务。这时,gorm会检查tx这个实例是否已经预编译过,如果是,则直接从缓存中获取结果,如果不是,则调用tx.Begin()方法来开启一个新的gorm.DB实例,并将其作为参数传递给回调函数。同时,gorm还会调用tx.Exec(“SAVEPOINT sp”)方法来创建一个保存点sp,并将其保存到tx2这个实例中。
- 在回调函数中,我们可以正常地执行CRUD操作,并检查是否有错误发生。如果有错误,则需要调用tx2.Rollback()方法来回滚子事务。这时,gorm会检查tx2这个实例是否有保存点sp,如果有,则调用tx.Exec(“ROLLBACK TO sp”)方法来回滚到保存点sp处,如果没有,则直接回滚整个事务。
- 最后,在所有的子事务都成功提交之后,我们需要返回nil来表示外层的事务也成功提交。这时,gorm会调用db.Commit()方法来提交整个事务
// Transaction 方法源码
func (db *DB) Transaction(fc func(tx *DB) error, opts ...*sql.TxOptions) (err error) {
panicked := true // 定义一个标志变量,表示是否发生了panic
if committer, ok := db.Statement.ConnPool.(TxCommitter); ok && committer != nil {
// 如果当前的连接池是一个事务提交器,并且不为空,那么说明已经存在一个事务
// 这时候就需要创建一个嵌套事务
if !db.DisableNestedTransaction {
// 如果没有禁用嵌套事务,那么就创建一个保存点,用于在后续回滚到某个状态
// 保存点的名字是根据回调函数的内存地址生成的,保证唯一性
err = db.SavePoint(fmt.Sprintf("sp%p", fc)).Error
if err != nil {
return // 如果创建保存点失败,那么直接返回错误
}
defer func() {
// 使用延迟函数,在函数结束之前执行以下操作
// 如果发生了panic,或者有错误返回,那么就回滚到保存点处
if panicked || err != nil {
db.RollbackTo(fmt.Sprintf("sp%p", fc))
}
}()
}
// 调用回调函数,并传入一个新的*gorm.DB实例作为参数
// 这个实例是基于当前实例复制的,但是不会复用之前的Statement
err = fc(db.Session(&Session{NewDB: db.clone == 1}))
} else {
// 如果当前的连接池不是一个事务提交器,或者为空,那么说明不存在一个事务
// 这时候就需要开启一个新的事务
tx := db.Begin(opts...) // 调用Begin方法,并传入可选的事务选项,开启一个新的*gorm.DB实例作为事务句柄
if tx.Error != nil {
return tx.Error // 如果开启事务失败,那么直接返回错误
}
defer func() {
// 使用延迟函数,在函数结束之前执行以下操作
// 如果发生了panic,或者有错误返回,那么就回滚整个事务
if panicked || err != nil {
tx.Rollback()
}
}()
// 调用回调函数,并传入事务句柄作为参数,如果没有错误返回,那么就提交整个事务
if err = fc(tx); err == nil {
panicked = false // 将标志变量设置为false,表示没有发生panic
return tx.Commit().Error // 返回提交事务的结果
}
}
panicked = false // 将标志变量设置为false,表示没有发生panic
return // 返回错误结果
}
3. GORM事物传播
- 首先gorm支持以下几种传播机制,默认使用REQUIRED
- REQUIRED:如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。这是默认的传播机制。
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- NESTED:如果当前存在事务,则创建一个嵌套事务作为当前事务的子事务;如果当前不存在事务,则创建一个新的事务。嵌套事务可以独立于父事务进行提交或回滚。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务方式执行。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则把当前事务挂起。
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
- 在gorm中,当你调用Begin()函数,获取事务句柄tx时,Begin()函数返回的事务传播机制是REQUIRED,也就是如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。这是默认的传播机制
tx := db.Begin() // 新建事物
- 假设已经拿到了事物句柄tx, 通过一个tx操作多张表时,同一个tx使用的是REQUIRED传播机制,也就是如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务,如果在一个已经存在的事务句柄tx上再次调用Begin()函数,注意是在一下tx上再次调用Begin()拿到新的tx2那么就不是加入该事务,而是创建一个新的事务句柄tx2,这个新的事务句柄tx2和原来的事务句柄tx是完全独立的,互不影响
- 那么如何修改使用不同的传播机制,这里首先要了解gorm下的Session结构体,在事物传播角度重点关注:
- 内部有一个NewDB 是否创建一个新的数据库连接属性
- SkipDefaultTransaction 是否跳过默认的事务属性
- DisableNestedTransaction 是否禁用嵌套谁无
type Session struct {
DryRun bool // 是否执行干运行(不实际修改数据库)
PrepareStmt bool // 是否为查询准备预编译的语句
NewDB bool // 是否创建一个新的数据库连接,在REQUIRES_NEW事务传播级别中使用
Initialized bool // 是否已初始化
SkipHooks bool // 在创建、更新、删除等操作时是否跳过钩子函数
SkipDefaultTransaction bool // 是否跳过默认的事务
DisableNestedTransaction bool // 是否禁用嵌套事务
AllowGlobalUpdate bool // 是否允许全局更新(更新所有匹配记录)
FullSaveAssociations bool // 是否保存全部关联记录
QueryFields bool // 是否查询字段信息
Context context.Context // 上下文
Logger logger.Interface // 日志记录器接口
NowFunc func() time.Time // 当前时间的函数
CreateBatchSize int // 批量创建操作时,每批次的大小
}
- 无事物案例
// 无事务
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.Create(&user1) // 不会自动开启事务
tx.Begin().Create(&user2).Commit() // 手动开启并提交事务
- 手动事物与事物传播控制案例
- Begin()函数返回的事务传播机制是REQUIRED,也就是如果当前存在事务,则加入该事务;如果当前不存在事务,那么就创建一个新的事务。这个说法是正确的。但是,如果在一个已经存在的事务句柄tx上再次调用Begin()函数,那么就不是加入该事务,而是创建一个新的事务句柄tx2,这个新的事务句柄tx2和原来的事务句柄tx是完全独立的,互不影响。这相当于实现了新建事务,也就是PROPAGATION_REQUIRES_NEW传播级别。所以,tx2回滚时不会影响到user1,因为它们是不同的事务
// 新建事务
tx := db.Begin()
tx.Create(&user1)
tx2 := tx.Begin()
tx2.Create(&user2)
tx2.Rollback() // 回滚 user2
tx.Commit() // 提交 user1
- Transaction嵌套事物与事物传播案例
- db.Transaction()是一个外层的事务,它创建了user1。然后它又执行了两个内部的子事务,分别是tx.Transaction()和tx2.Transaction()。第一个子事务创建了user2,但是返回了一个错误,导致回滚user2。第二个子事务创建了user3,并且成功提交
- user1和user3被创建并保存到数据库中,而user2被回滚并丢弃。tx2的回滚并没有应用到user1上,因为它们是不同的子事务
// 嵌套事务
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") // 回滚 user2
})
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user3)
return nil
})
return nil
}) // 提交 user1, user3
4. SavePoint 和 RollbackTo
- GORM 操作数据库时,有时需要在事务中实现更细粒度的操作。使用 SavePoint 和 RollbackTo 可以在事务中实现对单个操作的回滚而不影响其他操作
- SavePoint用来创建一个保存点,并将当前事务状态保存下来,以便在后续的操作中可以回滚到这个保存点。
- RollbackTo在事务中回滚到指定的保存点,并撤销指定保存点之后的所有操作
- 示例
- 在操作事物时可以通过SavePoint(savepoint)设置保存点
- 在事物执行过程中如果需要回滚可以通过RollbackTo(savepoint)回滚到指定保存点
// 开启一个事务
err := db.Transaction(func(tx *gorm.DB) error {
// 执行一些数据库操作
// 创建一个保存点
savepoint := "save_point_1"
err := tx.Session(&gorm.Session{PrepareStmt: true}).
SavePoint(savepoint).Error
if err != nil {
return err
}
// 进行更多的数据库操作
// 需要回滚到保存点
err = tx.Session(&gorm.Session{PrepareStmt: true}).
RollbackTo(savepoint).Error
if err != nil {
return err
}
// 回滚后的操作
return nil
})
5. 事物隔离
- gorm中的事务隔离级别主要有以下几种:(默认使用可重复读REPEATABLE READ)
- 读未提交READ UNCOMMITTED:最低的隔离级别,允许一个事务读取另一个事务未提交的数据,可能出现脏读dirty read,不可重复读non-repeatable read,幻读phantom read,丢失更新lost update等
- 读已提交READ COMMITTED:只允许一个事务读取另一个事务已提交的数据,避免了脏读问题。但是仍然会出现不可重复读和幻读等问题。
- 可重复读REPEATABLE READ:这是MySQL的默认隔离级别,它保证了在一个事务中多次读取同一数据时,结果是一致的,避免了不可重复读问题。但是仍然会出现幻读等问题。这种隔离级别对数据库性能影响较大,因为需要使用多版本并发控制(MVCC)来实现。
- 串行化SERIALIZABLE:这是最高的隔离级别,它保证了所有事务都按照顺序执行,避免了所有数据不一致的问题。但是,这种隔离级别对数据库性能影响最大,因为需要对所有数据进行加锁来防止并发访问。
- 修改隔离级别:
- 在调用gorm的Open()函数创建数据库连接句柄时可以通过tx_isolation设置隔离级别
- 在操作数据库时可以通过数据库句柄调用Clauses()通过.Locking的Strength属性设置单个事务的隔离级别
- 在执行sql语句时可以通过DB的Exec()执行"SET TRANSACTION ISOLATION LEVEL 指定隔离级别"设置隔离级别
// 读未提交READ UNCOMMITTED
// 允许一个事务读取另一个事务未提交的数据,也就是脏读dirty read
// 在初始化时设置全局的事务隔离级别为读未提交
db, _ = gorm.Open(mysql.New(mysql.Config{
DSN: "user:pass@tcp(localhost:3306)/dbname?parseTime=true&tx_isolation='READ-UNCOMMITTED'",
}))
// 读已提交READ COMMITTED
// 只允许一个事务读取另一个事务已提交的数据,避免了脏读问题
// 在创建事务时设置单个事务的隔离级别为读已提交
tx := db.Clauses(clause.Locking{Strength: "READ COMMITTED"}).Begin()
tx.Create(&user1)
tx.Commit()
// 可重复读REPEATABLE READ
// 保证了在一个事务中多次读取同一数据时,结果是一致的,避免了不可重复读问题
// 这是MySQL的默认隔离级别,不需要特别设置
// 串行化SERIALIZABLE
// 保证了所有事务都按照顺序执行,避免了所有数据不一致的问题
// 在执行SQL语句时设置当前会话的事务隔离级别为串行化
db.Exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
db.Create(&user1)
db.Commit()
GORM 优化
1. 批量插入优化
- 批量插入优化: 直接调用 Create()进行批量插入时每次Create()都会与数据库建立一次连接,执行一次查询,性能损耗大
- 通过CreateInBatches()优化
- 将批量插入的数据存入一个数组中
- 通过CreateInBatches()函数一次插入多条
- CreateInBatches() 会自动拆分数据为多个批次,每批次使用一条 INSERT 语句插入
// 批量构建插入数据
var users []User
for i := 0; i < 100; i++ {
users = append(users, User{Name: "user"})
}
// 使用一条sql插入所有数据
db.CreateInBatches(users, 1000)
- 基于保存点的方式优化
- 调用Session()函数创建一个保存点
- 执行多次Create(), 最后统一一次Commit()提交插入
- PrepareStmt 保存点会缓存每个 INSERT 语句,执行时一次性执行所有语句
// 创建一个保存点
tx := db.Session(&gorm.Session{PrepareStmt: true})
// 循环插入
for _, user := range users {
tx.Create(&user)
}
// 提交事务,一次性执行所有sql
tx.Commit()
2. 预编译优化
- gorm提供对sql语言进行预编译的函数Prepare(),会返回一个Stmt指针,可以通过返回的stmt给编译sql绑定数据
- 预编译的优点:
- SQL 语句只解析一次,性能更好
- 参数与 SQL 分离,可以有效避免 SQL 注入
func main() {
// 连接数据库
db, err := gorm.Open("mysql", "...")
// 预编译 INSERT 语句
stmt, err := db.DB().Prepare("INSERT INTO users (name, age) VALUES (?, ?)")
// 开始循环插入多行数据
for i := 0; i < 10; i++ {
// 为预编译语句绑定参数,执行插入
res, err := stmt.Exec("John"+string(i), i+10)
}
// 关闭语句
stmt.Close()
// 提交事务
db.Commit()
// 关闭数据库连接
db.Close()
}