一. 初始化
- 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{})
}
二. 增删改查示例
- 增删改查接口示例
- 编写对应指定表的结构体
- 针对指定表编写增删改查接口
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"
}
func (c *DBSiteApplication) GetCardNum() string {
return sensitive.Decode(c.CardNumEn)
}
func (c *DBSiteApplication) GetMobile() string {
return sensitive.Decode(c.MobileEn)
}
//Add 添加记录
func (c *DBSiteApplication) Add() (lmerr *lmerror.LmError) {
db := DB
if db == nil {
return lmerror.NewLmError(lmerror.DB_CONNECT_ERROR, "DBSiteApplication.Add Error DB Is Nil")
}
err := db.Create(c).Error
if err != nil {
return lmerror.NewLmError(lmerror.DB_INSERT_ERROR, "DBSiteApplication.Add Error "+err.Error())
}
return nil
}
//Update 只修改带参结构
func (c *DBSiteApplication) Update() (lmerr *lmerror.LmError) {
db := DB
if db == nil {
str, _ := json.Marshal(c)
return lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.Update Error id = "+string(str))
}
err := db.Model(c).Update(c).Error
if err != nil {
str, _ := json.Marshal(c)
return lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.Update Error id = "+string(str)+" | "+err.Error())
}
return nil
}
//UpdateSave 全修改
func (c *DBSiteApplication) UpdateSave() (lmerr *lmerror.LmError) {
db := DB
if db == nil {
str, _ := json.Marshal(c)
return lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.UpdateSave Error id = "+string(str))
}
err := db.Model(c).Save(c).Error
if err != nil {
str, _ := json.Marshal(c)
return lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.UpdateSave Error id = "+string(str)+" | "+err.Error())
}
return nil
}
//根据结构体查询并映射
func (c *DBSiteApplication) Search() (lmerr *lmerror.LmError) {
db := DB
if db == nil {
str, _ := json.Marshal(c)
return lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.Search Error id = "+string(str))
}
err := db.Model(c).Where(c).First(c).Error
if err != nil {
str, _ := json.Marshal(c)
return lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.Search Error id = "+string(str)+" | "+err.Error())
}
return nil
}
func SelectSiteApplicationById(applicationId int) (*DBSiteApplication, *lmerror.LmError) {
db := DB
if db == nil {
return nil, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "SelectSiteApplicationById DB error")
}
var siteApplication DBSiteApplication
//条件查询
err := db.Where("application_id = ? ", applicationId).Find(&siteApplication).Error
if err != nil {
return nil, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, err.Error())
}
return &siteApplication, nil
}
func SelectSiteApplicationByCode(siteCode string, managementType, status int) (*DBSiteApplication, *lmerror.LmError) {
db := DB
if db == nil {
return nil, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "SelectSiteApplicationByCode DB 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, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, err.Error())
}
return &siteApplication, nil
}
//支持SiteAddress模糊
func (c *DBSiteApplication) SelectByConditionsValid() ([]DBSiteApplication, *lmerror.LmError) {
db := DB
if db == nil {
return nil, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.SelectByStruct DB is nil")
}
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, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.SelectByStruct Error |"+err.Error())
}
return applicationList, nil
}
func (c *DBSiteApplication) SelectByPageValid(pageNo, pageSize int) ([]DBSiteApplication, int, *lmerror.LmError) {
db := DB
if db == nil {
return nil, 0, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.SelectByPageValid DB is nil")
}
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 方法之前调用,否则会出现错误。
lmErr := db.Count(&count).Limit(limit).Offset(offset).Find(&applicationList).Error
if lmErr != nil {
return nil, 0, lmerror.NewLmError(lmerror.DB_QUERY_ERROR, "DBSiteApplication.SelectByPageValid Error |"+lmErr.Error())
}
return applicationList, count, nil
}
- 执行接口
func SiteApplicationQueryPage(msgid string, request req.QueryPage) (*entity.PageResp, *lmerror.LmError) {
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
}
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. GORM事物传播
- gorm有以下几种传播机制
//继续使用当前事物,如果当前没有事物则报错
REQUIRED
//继续使用当前事物,如果当前没有事物,使用自己的事物
SUPPORTS
//总是新建一个事物,独立于当前事物
REQUIRES_NEW
//继续当前事物,如果当前没有事物则报错
MANDATORY
- REQUIRES_NEW: 使用grom时获取到数据库连接后,调用Begin()函数,获取事物句柄tx,Begin()函数返回的事物传播机制就是REQUIRES_NEW新建一个事物
tx := db.Begin() // 新建事物
- SUPPORTS默认: 假设已经拿到了事物句柄tx, 通过一个tx操作多张表时,同一个tx下都是SUPPORTS继续使用当前事物,如果当前没有事物,使用自己的事物
- 其它事物隔离级别可以通过gorm的set()函数设置"query_options"来设置
- query_options为FOR UPDATE 表示使用 REQUIRED传播
- query_options为LOCK IN SHARE MODE表示使用 MANDATORY传播
其他值:SUPPORTS传播 (默认)
- 示例: 设置REQUIRED:继续使用当前事物,如果当前没有事物则报错,可以通过数据库连接,调用set函数设置
//REQUIRED 传播
db.Set("gorm:query_options", "FOR UPDATE").First(&user)
//MANDATORY传播
db.Set("gorm:query_options", "LOCK IN SHARE MODE").First(&user)
2. SavePoint 和 RollbackTo
- GORM 操作数据库时,有时需要在事务中实现更细粒度的操作。使用 SavePoint 和 RollbackTo 可以在事务中实现对单个操作的回滚而不影响其他操作
- SavePoint用来创建一个保存点,并将当前事务状态保存下来,以便在后续的操作中可以回滚到这个保存点。
- RollbackTo在事务中回滚到指定的保存点,并撤销指定保存点之后的所有操作
3. 如何优雅的处理事物
- 像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)
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()
}
四. 总结
- 参考博客