go 进阶 三方库之 gorm

一. 初始化

  1. gorm: go中一个比较方便操作数据库的三方库
  2. 初始化连接
  1. 只给go get 命令,拉取mysql驱动与gorm包
  2. 读取数据库配置信息,调用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{})
}

二. 增删改查示例

  1. 增删改查接口示例
  1. 编写对应指定表的结构体
  2. 针对指定表编写增删改查接口
//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
}
  1. 执行接口
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	列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 boolintuint、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区别

  1. Create 用于创建新的记录并向数据库中插入一条新数据
  2. Save 主要用于保存数据,如果记录不存在,则会创建新的记录;如果记录存在则会更新该记录的所有字段
  3. 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支持

  1. 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 与锁

  1. gorm 中,提供了多种类型的锁,包括行级锁(Row-level Lock)、表级锁(Table-level Lock)、共享锁(Shared Lock)和排它锁(Exclusive Lock)等。其中,最常用的是共享锁和排它锁
  2. 共享锁(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
}
  1. 排它锁(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
}
  1. gorm 还提供了一些其他的锁类型,例如 NOWAIT 、 SKIP LOCKED 、 SHARE ROW EXCLUSIVE 等

GORM的预加载Preload与Joins

  1. gorm中提供了预加载功能Preload(),查看一对一中的使用示例
  2. 预加载也分为懒汉式与饿汉式两种,可以通过设置 AutoPreload 来开启 Lazy Loading 功能(可能会导致数据冗余和查询性能下降等问题)
db = db.Set("gorm:auto_preload", true)
  1. 还有一种是选择性预加载可以通过 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
}
  1. 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
}
//......

查询时优雅的处理动态条件

  1. gorm 支持使用结构体、map 和字符串三种方式来构建查询条件
  2. 字符串方式上方有就不再进行示例
  3. 结构体查询条件示例
// 定义查询条件结构体
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 {
    // 处理错误
}
  1. 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 {
    // 处理错误
}
  1. 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 {
    // 处理错误
}

分页

  1. 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 {
    // 处理错误
}
  1. 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 {
    // 处理错误
}
  1. 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扩展包

  1. gorm v2.x 版本提供了一个扩展包 gorm.io/plugin,其中包含了许多实用的工具和插件,以下是其中一些常用的扩展工具:
  1. Audit 插件:记录数据变更历史,包括创建时间、更新时间、删除时间以及操作者。
  2. SoftDelete 插件:在删除数据时进行软删除,即将删除标记设置为 true 而不是从数据库中直接删除数据。
  3. Preload 插件:使用预加载机制优化关联查询,避免 N + 1 查询问题。
  4. Scopes 插件:使用作用域模式对查询条件进行封装和复用。
  5. UUID 类型支持:在类型映射中增加了对 UUID 类型的支持。
  6. Paginate 插件:对分页查询进行封装,使得代码更加简洁和易读。
  1. 除了上述扩展工具之外,还有一些其他的实用扩展工具,如 Debug、Import、Interceptors 等。需要根据实际需求进行选择和使用。

三. 问题

GORM 如何实现一对多和多对多关系的映射

关联标签
在这里插入图片描述

1. 一对一

  1. gorm中BelongsTo 或 HasOne 定义一对一关系,HasMany 定义一对多关系,多对多关系可以使用 ManyToMany 方法或 HasMany 方法来定义
  2. 一对一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 {
    // 处理错误
}
  1. Preload 方法预加载注意事项:应该尽量避免传入一个包含多层嵌套关系的字符串参数(如 “Profile.Address”),会导致预加载的效果不佳,甚至可能出现错误。如果需要预加载多个关联模型,可以在调用 Preload 方法时多次传入参数,例如
var user User
result := db.Preload("Profile").Preload("Orders").First(&user, id)
if result.Error != nil {
    // 处理错误
}

2. 一对多

  1. 一对多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 {
    // 处理错误
}
  1. 如果要进行自定义的条件过滤和排序,可以使用 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 {
    // 处理错误
}
  1. 需要注意的是,在一对多关联中,如果要通过关联字段进行查询和过滤,可以使用 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. 多对多

  1. 对于多对多关系,可以使用 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 查询的问题

  1. N+1 查询问题指的是在查询关联表时,如果使用了嵌套循环进行查询,就会产生大量的 SQL 查询。为了避免这个问题,可以使用 GORM 的 Preload 方法预先加载关联数据
// 查询 Users 以及它们的 Addresses
//这个查询会一次性加载所有 User 和 Address 数据,避免了 N+1 查询问题
var users []User
DB.Preload("Addresses").Find(&users)

GORM 的 Preload 方法和 Joins 方法有什么区别

  1. Preload 方法是在查询时预加载关联数据,而 Joins 方法是通过 SQL JOIN 语句连接多个表查询数据。Preload 方法适用于关联表较少、数据量不大的情况;而 Joins 方法适用于关联表较多、数据量较大的情况

如何在 GORM 中使用原生 SQL 查询

  1. 可以使用 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 {
    // 处理错误
}
  1. 还可以使用 Exec 方法来执行不需要返回值的 SQL 查询,如插入、更新或删除数据
result := db.Exec("DELETE FROM users WHERE age < ?", 18)
if result.Error != nil {
    // 处理错误
}

GORM与事物

  1. 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. 如何优雅的处理事物

  1. 像java框架中使用@Transactional可以使事物代码与业务代码解耦,go中怎么操作?
  2. 以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嵌套事物

  1. Transaction使用示例
  1. db.Transaction()方法接受一个函数作为参数,这个函数也接受一个*gorm.DB类型的参数,表示当前的事务对象。这个函数需要返回一个error类型的值,表示事务是否成功或失败。
  2. db.Transaction()方法在内部会调用db.Begin()方法来开启一个新的事务,并将事务对象作为参数传递给函数。
  3. 函数在内部可以使用事务对象来执行一些数据库操作,比如增删改查等。如果所有的操作都成功,则返回nil表示事务提交;如果有任何操作失败,则返回错误表示事务回滚。
  4. db.Transaction()方法在接收到函数的返回值后,会根据返回值的类型来决定是调用tx.Commit()方法来提交事务,还是调用tx.Rollback()方法来回滚事务。
  5. 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
}
  1. 嵌套事物的底层原理:
  1. 使用db.Transaction(func(tx gorm.DB) error {…})方法来实现嵌套事物,该方法需接收一个执行数据库相关操作的回调函数,gorm会调用db.Begin()方法来开启一个新的gorm.DB实例,并将其作为参数传递给回调函数
  2. 在回调函数中,也就是Transaction内部执行数据库操作的函数,可以再次调用tx.Transaction(func(tx2 gorm.DB) error {…})方法来执行一个内部的子事务。这时,gorm会检查tx这个实例是否已经预编译过,如果是,则直接从缓存中获取结果,如果不是,则调用tx.Begin()方法来开启一个新的gorm.DB实例,并将其作为参数传递给回调函数。同时,gorm还会调用tx.Exec(“SAVEPOINT sp”)方法来创建一个保存点sp,并将其保存到tx2这个实例中。
  3. 在回调函数中,我们可以正常地执行CRUD操作,并检查是否有错误发生。如果有错误,则需要调用tx2.Rollback()方法来回滚子事务。这时,gorm会检查tx2这个实例是否有保存点sp,如果有,则调用tx.Exec(“ROLLBACK TO sp”)方法来回滚到保存点sp处,如果没有,则直接回滚整个事务。
  4. 最后,在所有的子事务都成功提交之后,我们需要返回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事物传播

  1. 首先gorm支持以下几种传播机制,默认使用REQUIRED
  1. REQUIRED:如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。这是默认的传播机制。
  2. REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  3. NESTED:如果当前存在事务,则创建一个嵌套事务作为当前事务的子事务;如果当前不存在事务,则创建一个新的事务。嵌套事务可以独立于父事务进行提交或回滚。
  4. SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务方式执行。
  5. NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则把当前事务挂起。
  6. NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. MANDATORY:如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  1. 在gorm中,当你调用Begin()函数,获取事务句柄tx时,Begin()函数返回的事务传播机制是REQUIRED,也就是如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务。这是默认的传播机制
tx := db.Begin() // 新建事物
  1. 假设已经拿到了事物句柄tx, 通过一个tx操作多张表时,同一个tx使用的是REQUIRED传播机制,也就是如果当前存在事务,则加入该事务;如果当前不存在事务,则创建一个新的事务,如果在一个已经存在的事务句柄tx上再次调用Begin()函数,注意是在一下tx上再次调用Begin()拿到新的tx2那么就不是加入该事务,而是创建一个新的事务句柄tx2,这个新的事务句柄tx2和原来的事务句柄tx是完全独立的,互不影响
  2. 那么如何修改使用不同的传播机制,这里首先要了解gorm下的Session结构体,在事物传播角度重点关注:
  1. 内部有一个NewDB 是否创建一个新的数据库连接属性
  2. SkipDefaultTransaction 是否跳过默认的事务属性
  3. 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              // 批量创建操作时,每批次的大小
}
  1. 无事物案例
	// 无事务
	tx := db.Session(&Session{SkipDefaultTransaction: true})
	tx.Create(&user1) // 不会自动开启事务
	tx.Begin().Create(&user2).Commit() // 手动开启并提交事务
  1. 手动事物与事物传播控制案例
  1. 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
  1. Transaction嵌套事物与事物传播案例
  1. db.Transaction()是一个外层的事务,它创建了user1。然后它又执行了两个内部的子事务,分别是tx.Transaction()和tx2.Transaction()。第一个子事务创建了user2,但是返回了一个错误,导致回滚user2。第二个子事务创建了user3,并且成功提交
  2. 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

  1. GORM 操作数据库时,有时需要在事务中实现更细粒度的操作。使用 SavePoint 和 RollbackTo 可以在事务中实现对单个操作的回滚而不影响其他操作
  1. SavePoint用来创建一个保存点,并将当前事务状态保存下来,以便在后续的操作中可以回滚到这个保存点。
  2. RollbackTo在事务中回滚到指定的保存点,并撤销指定保存点之后的所有操作
  1. 示例
  1. 在操作事物时可以通过SavePoint(savepoint)设置保存点
  2. 在事物执行过程中如果需要回滚可以通过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. 事物隔离

  1. gorm中的事务隔离级别主要有以下几种:(默认使用可重复读REPEATABLE READ)
  1. 读未提交READ UNCOMMITTED:最低的隔离级别,允许一个事务读取另一个事务未提交的数据,可能出现脏读dirty read,不可重复读non-repeatable read,幻读phantom read,丢失更新lost update等
  2. 读已提交READ COMMITTED:只允许一个事务读取另一个事务已提交的数据,避免了脏读问题。但是仍然会出现不可重复读和幻读等问题。
  3. 可重复读REPEATABLE READ:这是MySQL的默认隔离级别,它保证了在一个事务中多次读取同一数据时,结果是一致的,避免了不可重复读问题。但是仍然会出现幻读等问题。这种隔离级别对数据库性能影响较大,因为需要使用多版本并发控制(MVCC)来实现。
  4. 串行化SERIALIZABLE:这是最高的隔离级别,它保证了所有事务都按照顺序执行,避免了所有数据不一致的问题。但是,这种隔离级别对数据库性能影响最大,因为需要对所有数据进行加锁来防止并发访问。
  1. 修改隔离级别:
  1. 在调用gorm的Open()函数创建数据库连接句柄时可以通过tx_isolation设置隔离级别
  2. 在操作数据库时可以通过数据库句柄调用Clauses()通过.Locking的Strength属性设置单个事务的隔离级别
  3. 在执行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. 批量插入优化

  1. 批量插入优化: 直接调用 Create()进行批量插入时每次Create()都会与数据库建立一次连接,执行一次查询,性能损耗大
  2. 通过CreateInBatches()优化
  1. 将批量插入的数据存入一个数组中
  2. 通过CreateInBatches()函数一次插入多条
  3. CreateInBatches() 会自动拆分数据为多个批次,每批次使用一条 INSERT 语句插入
// 批量构建插入数据 
var users []User
for i := 0; i < 100; i++ {
    users = append(users, User{Name: "user"}) 
}

// 使用一条sql插入所有数据
db.CreateInBatches(users, 1000) 
  1. 基于保存点的方式优化
  1. 调用Session()函数创建一个保存点
  2. 执行多次Create(), 最后统一一次Commit()提交插入
  3. PrepareStmt 保存点会缓存每个 INSERT 语句,执行时一次性执行所有语句
// 创建一个保存点
tx := db.Session(&gorm.Session{PrepareStmt: true})

// 循环插入
for _, user := range users {
    tx.Create(&user)
}

// 提交事务,一次性执行所有sql
tx.Commit()

2. 预编译优化

  1. gorm提供对sql语言进行预编译的函数Prepare(),会返回一个Stmt指针,可以通过返回的stmt给编译sql绑定数据
  2. 预编译的优点:
  1. SQL 语句只解析一次,性能更好
  2. 参数与 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()    
}

四. 总结

  1. 参考博客
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值