go 进阶 三方库之 gorm

2023-11-14

一. 初始化

  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. 针对指定表编写增删改查接口
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
}
  1. 执行接口
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区别

  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. GORM事物传播

  1. gorm有以下几种传播机制
//继续使用当前事物,如果当前没有事物则报错
REQUIRED 
//继续使用当前事物,如果当前没有事物,使用自己的事物 
SUPPORTS  
//总是新建一个事物,独立于当前事物 
REQUIRES_NEW
//继续当前事物,如果当前没有事物则报错 
MANDATORY
  1. REQUIRES_NEW: 使用grom时获取到数据库连接后,调用Begin()函数,获取事物句柄tx,Begin()函数返回的事物传播机制就是REQUIRES_NEW新建一个事物
tx := db.Begin() // 新建事物
  1. SUPPORTS默认: 假设已经拿到了事物句柄tx, 通过一个tx操作多张表时,同一个tx下都是SUPPORTS继续使用当前事物,如果当前没有事物,使用自己的事物
  2. 其它事物隔离级别可以通过gorm的set()函数设置"query_options"来设置
  1. query_options为FOR UPDATE 表示使用 REQUIRED传播
  2. query_options为LOCK IN SHARE MODE表示使用 MANDATORY传播
    其他值:SUPPORTS传播 (默认)
  1. 示例: 设置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

  1. GORM 操作数据库时,有时需要在事务中实现更细粒度的操作。使用 SavePoint 和 RollbackTo 可以在事务中实现对单个操作的回滚而不影响其他操作
  1. SavePoint用来创建一个保存点,并将当前事务状态保存下来,以便在后续的操作中可以回滚到这个保存点。
  2. RollbackTo在事务中回滚到指定的保存点,并撤销指定保存点之后的所有操作

3. 如何优雅的处理事物

  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)

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. 参考博客
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go 进阶 三方库之 gorm 的相关文章

  • “create_date”时间戳字段的默认值无效

    我有以下 sql 创建语句 mysql gt CREATE TABLE IF NOT EXISTS erp je menus gt id INT 11 NOT NULL AUTO INCREMENT gt name VARCHAR 100
  • Laravel 读写连接不同步

    我在 Laravel 5 2 应用程序中使用读写 MySQL 连接设置 mysql gt write gt host gt env DB HOST WRITE localhost read gt host gt env DB HOST RE
  • MySQL 连接器 C++ 64 位在 Visual Studio 2012 中从源代码构建

    我正在尝试建立mySQL 连接器 C 从源头在视觉工作室2012为了64 bit建筑学 我知道这取决于一些boost头文件和C 连接器 跑步CMake生成一个项目文件 但该项目文件无法编译 因为有一大堆非常令人困惑的错误 这些错误可能与包含
  • parent_id 是外键(自引用)并且为 null?

    浏览 Bill Karwin 的书 SQL Antipatterns 第 3 章 Naive Trees 邻接表 父子关系 有一个注释表的示例 CREATE TABLE Comments comment id SERIAL PRIMARY
  • 如何对字段数据进行分组?

    我有 sql 查询来显示数据 SELECT artikel foto naam fotografer id fotografer name fotografer customer first name customer last name
  • 如何处理多个连接

    我有一个复杂的查询 需要总共 4 个表中的字段 内部联接导致查询花费的时间比应有的时间长得多 我已经运行了一个 EXPLAIN 语句 其可视化结果附在下面 这是我的查询 SELECT pending corrections correcte
  • 如何在chart.js中使用JSON数据?

    您好 我一直在尝试使用 MYSQL 数据库中的数据 并使用它们通过 Chart js 创建图形图表 我将数据编码为 JSON 数据 通过 php 文件名 data1 php 现在我需要使用 Jquery 或 javascript 将这些 J
  • 使用 MySQL 计算时间线中的变化

    我是 MySQL 新手 我需要你的帮助 我有一个包含类似数据的表 RobotPosX RobotPosY RobotPosDir RobotShortestPath 0 1 0 2 15 1456 0 2 0 3 30 1456 0 54
  • MySQL Amazon RDS:超出锁定等待超时

    在 Mysql Amazon RDS 上 当我尝试运行以下 SQL 查询时 UPDATE table1 INNER JOIN table2 USING CommonColumn SET table1 col1 table2 x table1
  • 对不同的数据库和表进行一次性查询

    我有一个 Rails 应用程序 侧面有一个 WordPress 博客 完全单独位于 blog 客户想要 Rails 应用程序主页上的最新博客文章 因此我需要对 word press 数据库进行一次性 mysql 查询 我将如何在 Rails
  • MySQL 查询按父级排序然后子级排序

    我的数据库中有一个页面表 每个页面可以有一个父页面 如下所示 id parent id title 1 0 Home 2 0 Sitemap 3 0 Products 4 3 Product 1 5 3 Product 2 6 4 Prod
  • Python中的MariaDB连接器无法连接到远程服务器

    我使用与远程 Mariadb 服务器的连接已有几个月了 今天 无法再通过 macOS 上的 python mariadb 模块和 mariadb 连接器建立连接 基本安装如下 brew install mariadb connector c
  • InnoDB:使用事务批量插入或组合多个查询?

    做批量的时候INSERT在InnoDB中 我应该使用事务吗 START TRANSACTION INSERT INTO tbl name a b c VALUES 1 2 3 INSERT INTO tbl name a b c VALUE
  • 在生产数据库上部署应用程序时无法加载文件或程序集“System.Data”错误

    问题 将我的应用程序部署到生产数据库时 出现以下错误 无法加载文件或程序集 System Data Version 2 0 0 0 Culture neutral PublicKeyToken b77a5c561934e089 或其依赖项之
  • MySql If then 在 Select 语句中

    我想在 mysql select 中使用 IF THEN 语句 但无法弄清楚 当还没有评论时 commentcreated 值应该是该项目本身的创建值 这是查询 SELECT item count comments itemid AS co
  • 地理位置邻近搜索

    应用程序中有一个要求 要求在谷歌地图中找出与特定国家和 或城市绑定的所有对象 我们使用谷歌地图 API 预先计算了具有各自纬度和经度的对象并将其存储在数据库中 有时 这些对象在它们可以提供服务的特定服务范围 半径内提供服务 例如现在的情况就
  • MySQL JDBC 连接上的故障转移?

    我正在尝试确定如何使用 MySQL JDBC 驱动程序实现高可用性解决方案 似乎有一个我可以设置的故障转移属性 但我想知道当使用 MySQL 和 JDBC 实现简单的故障转移机制时 人们倾向于使用什么 我们计划将 2 个前端 Tomcat
  • BIT(1) 的存储大小是多少?

    我一直认为a的存储大小BIT 1 列为 1 位 But http dev mysql com doc refman 5 6 en storage requirements html http dev mysql com doc refman
  • 检查字段是否为空

    如果我想检查该字段是否有除 null 和空之外的其他字符 查询是否正确 select CASE WHEN description IS NULL THEN null WHEN description IS NOT NULL THEN not
  • 如何从 mysqldump 中删除表

    如何删除 mysqldump 中包含大量表的一个大表的输出 我有一个 6 GB 大的数据库转储 但其中 90 只是一个日志记录表 cache entries 我的备份中不再需要它 如何轻松删除转储中描述大型日志记录表的部分 我找到了这个 h

随机推荐

  • JS(解构) 之数组和对象中提取数据总结

    解构含义 解构功能含义 从复杂数据类型中 数组或对象 中提取数据的过程 JS 解构 之数组 从数组中提取首个元素 方式一 基于数组下标提取元素 const names zzg zcx zcy const it names 0 console
  • CAPL编程实现诊断刷写,车联网FOTA流程自动化测试(代码篇)

    原创内容 转载请注明出处 接上篇 本文主要讲CAPL编程详细实现 软件环境CANoe 11 0 一 Simulation Setup 1 建模之前 首先创建一个 DBC文件 如果不会 可以用一个已有的DBC文件修改 新建待仿真的空节点 如下
  • Linux系统的启动流程

    一 开机启动流程图 第一步 开机自检就是开始工作之前先对自己的工具进行检查是否正常 BIOS就是主板上的一个自检程序 开机先对主板上自带的和外界的一些开机必备的设备进行检测 比如CPU 显卡 内存 硬盘等设备的自检过程就是自检 第二步 MB
  • 【斯坦福CS224W笔记之二】传统图机器学习的特征工程 — 节点

    Traditional Methods for ML on Graphs 是根据同济子豪兄学长的中文讲解做的笔记哦 感兴趣的话可以直接去b站观看详细视频 传送带 https github com TommyZihao zihao cours
  • Flask 框架

    目录 Flask介绍和安装 请求与响应 请求 响应 登录案例 配置文件写法 路由系统 路由写法 转换器 CBV session的使用和原理 flask session的使用 闪现flash 请求扩展 g对象 蓝图 小型蓝图 大型蓝图 数据库
  • 搭建AI智能语音外呼系统 智能语音外呼机器人

    随着人工智能技术的发展 近半年来涌现了大量基于人工智能的呼叫中心业务服务商和集成商 仅电销机器人这一个方向就至少有近百家公司正在推广运营 包括百度 讯飞 智齿 硅基 百应 箭鱼 容联等 商务上的需求非常强烈 整个市场都飞快地热闹起来 一套可
  • 小细节{变量名-枚举}

    一 类的变量名第一个字母一定要小写 eventType event type eventId 13 userId 45 openingFlag true Data TableName user activity AllArgsConstru
  • 基于matlab的车牌识别

    20221126 新增 首先说一下这个工程的思路 很多朋友妄想直接拿着工程用 那是不可能的 自己学去叭 我是先将车牌号预处理之后 整个图片干净一点之后 进行每个字符的切割 但是是很投机取巧的方法 是先切好第一个字符 再根据切割坐标 切割下一
  • 堆排序算法的具体分析和实现

    定义 堆就是完全二叉树的数据结构 堆排序是利用二叉树的孩子与双亲节点的比较来实现的排序方法 大顶堆 每个节点的值都大于或者等于它的左右子节点的值 小顶堆 每个节点的值都小于或者等于它的左右子节点的值 这里使用的是大顶堆 基本思想 堆排序的基
  • Meta 内部都在用的 FX 工具大起底:利用 Graph Transformation 优化 PyTorch 模型

    PyTorch 中的 graph mode 在性能方面表示更为出色 本文介绍 Torch FX 这个强大工具 可以捕捉和优化 PyTorch 程序 graph 一 简介 PyTorch 支持两种执行模式 eager mode 和 graph
  • 用Ai描摹图片

    用Ai描摹图片 陈子龙 2019 2 4 用ai来描摹这张图片 先用钢笔工具把哆啦A梦的外面黑的地方钩画出来 并上色 然后在把哆啦A梦的身体蓝色的地方用钢笔描出来 在把它白色的部位用钢笔描出
  • C语言中堆内存的申请和使用

    在编程过程中 有时需要使用大量数据 此时可以使用堆内存来方便存储和管理这些数据 堆内存是由程序员手动进行申请 释放的内存 它的空间非常大 但如果在申请后没有释放 会导致内存泄露 关于堆内存的常用函数 1 void malloc size t
  • 一文读懂微服务架构设计

    一 前言 微服务 MicroServices 是一种架构风格 一个大型复杂软件应用由多个微服务和前端展示层组成 系统中的各个微服务可被独立部署 各个微服务之间是松耦合的 每个微服务仅关注于完成一件任务并很好地完成该任务 在所有情况下 每个任
  • 组合预测模型

    组合预测模型 ARIMA CNN LSTM时间序列预测 Python 目录 组合预测模型 ARIMA CNN LSTM时间序列预测 Python 预测结果 基本介绍 程序设计 参考资料 预测结果 基本介绍 ARIMA CNN LSTM是一种
  • Django运行服务报NameError: name ‘os‘ is not defined-已解决

    这里调用了os模块 但是文件头并没引用os模块 解决办法 在settings py文件头加上 import os
  • 【MySQL】解决JDBC无法成功连接MySQL5.7的问题

    写在前面 笔者的个人主页近期升级了一下服务器 以前的VPS确实不行了 然后也就顺便用了最新版本也就是MySQL5 7 但是这个版本呢升级了很多安全策略 网上的资料 中文 也相对较少 之前因为安装这个MySQL5 7已经折腾了我大半天 这里附
  • CSS深入理解之line-height

    以下文字整理自慕课网 张鑫旭的 CSS深入理解之line height 我看到不时有人点赞收藏这篇文章 我想应该也有很多人是对line height 和vertical align 困惑吧 你们可以去看下这篇文章 上面有我学习vertica
  • texstudio更新记录

    Ubuntu20 04 更新TexStudio 本着不折腾不舒服的原则 准备更新texstudio 原版本2 12 22 texstudio网站上是没找到Ubuntu的 只有xubuntu版本的安装包 既然推荐用ppa方式 那就试试 点开紫
  • Windows Server间文件实时备份(syncthing) ---带历史版本“后悔药”

    一 概念简介 syncthing 一款开源免费的数据同步工具 基于P2P的跨平台文件同步工具 通过tcp建立设备连接 再通过TLS进行数据安全传输 支持公网与局域网搭建 支持单双向同步与历史版本控制 后悔药 支持Android Linux
  • go 进阶 三方库之 gorm

    目录 一 初始化 二 增删改查示例 Save与Update区别 GORM中的钩子 GORM Context支持 GORM 与锁 GORM的预加载Preload与Joins 查询时优雅的处理动态条件 分页 gorm io plugin扩展包