Golang----Gorm

1. Gorm 基本介绍

2.1 官方文档

https://gorm.io/zh_CN/

2.2 使用ORM的缺点

  1. 自动生成SQL语句会消耗计算资源,这势必会对程序性能造成一定的影响
  2. 对于复杂的数据库操作,ORM通常难以处理,自动生成的SQL 语句在性能方面也不如手写的原生SQL
  3. 生成SQL 语句的过程是自动进行的,不能人工干预,这是的开发人员无法定制一些特殊的SQL语句

2.环境准备

2.1 包下载

1. mysql的驱动

go get gorm.io/driver/mysql

2. Gorm

go get gorm.io/gorm

2.2 建立连接

连接格式: root:vansing2022@tcp(102.168.0.11:3306)/go_test?charset=utf8mb4&parseTime=True&loc=Local&timeout=18s

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func init() {
	username := "root"
	password := "vansing2022"
	host := "192.168.0.11"
	port := 3306
	Dbname := "go_test"
	timeout := "18s"
	clientString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
	db, err := gorm.Open(mysql.Open(clientString))
	if err != nil {
		panic("连接数据库失败,错误: " +  err.Error())
	}
	DB = db
}

func main() {
	fmt.Println(DB)
}

2.3 常用配置

2.3.1 关闭默认事务

为了保证数据一致性,GORM 会默认在写入操作(创建,更新,删除)开启事务执行,如果没有这方面的要求,可以在初始化的时候禁用它,这样可以获得60%的性能提升

	db, err := gorm.Open(mysql.Open(clientString),&gorm.Config{
		SkipDefaultTransaction: true,
	})

2.3.2 表结构命名策略

1. 默认情况

GORM 在数据库中创建表和字段时的命名策略是:表名是蛇形复数,字段名是蛇形单数

type Student struct {
    ID uint
	Name string
	Age int
	MyStudent string
}

在数据库中生成的表结构和字段为

CREATE TABLE `students` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,`age` bigint unsigned,`my_student` longtext,PRIMARY KEY (`id`))

2 .个性化配置

db, err := gorm.Open(mysql.Open(clientString), &gorm.Config{
    NamingStrategy: schema.NamingStrategy{
        TablePrefix:   "machine_", // 添加表名前缀
        SingularTable: false,      // 是否单数表名,默认为true,为复数表名
        NoLowerCase:   false,      // 是否小写转换,默认为true,为要小写转换
    },
})

2.4 显示日志

Gorm 的默认日志是只打印错误和慢 SQL

2.4.1 全局配置日志

	var mysqlLogger logger.Interface
	mysqlLogger = logger.Default.LogMode(logger.Info)   // 将日志级别改为info
	db, err := gorm.Open(mysql.Open(clientString), &gorm.Config{   // 全局配置日志
		Logger: mysqlLogger  	
	})

2.4.2 自定义日志(常用)

	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // 日志的输出目标,前缀和日志包含的内容
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值,即超过设定参数的sql查询都为慢SQL
			LogLevel:                  logger.Info, // 日志级别
			IgnoreRecordNotFoundError: true,        // 忽略 ErrRecordNotFound(记录未找到) 错误
			Colorful:                  true         // 使用控制台的彩色打印
		})

	db, err := gorm.Open(mysql.Open(clientString), &gorm.Config{
		Logger: newLogger, // 全局配置日志
	})

2.4.3 部分展示日志

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
)

var DB *gorm.DB
var mysqlLogger logger.Interface

func init() {
	username := "root"
	password := "vansing2022"
	host := "192.168.0.11"
	port := 3306
	Dbname := "go_test"
	timeout := "18s"
	// 1. 建立连接
	clientString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
	
    // 2. 配置gorm
	db, err := gorm.Open(mysql.Open(clientString), &gorm.Config{
	})
	if err != nil {
		panic("连接数据库失败,错误: " + err.Error())
	}
	DB = db
}

type Student struct {
	ID        uint
	Name      string
	Age       uint
	MyStudent string
}

func main() {
    
	DB = DB.Session(&gorm.Session{
		Logger: mysqlLogger,
	})
    DB.First(&Student)  // 通过DB,调用的所有函数都显示日志
}

2.4.4 某条语句显示日志

DB.Debug().AutoMigrate(&Student{})  // 表示自动创建模型的时候要显示日志
DB.Debug().First(&model)    	   // 表示查询第一个的时候显示日志

3. 模型定义

模型是标准的 struct,由Go 的基本数据类型,实现了 ScannerValuer 接口的自定义类型及其指针或别名组成

3.1 简单定义一张表

定义模型结构体,结构体名为Mysql表名,属性为Mysql字段

// 小写的属性名是不会在数据库生成字段的,因为外部文件访问不到小写属性名
type Student struct {
	ID    uint    // 默认使用ID作为主键
	Name  string
	Age   int
	Email *string // 使用指针是为了可以存空值
}

3.2 存储空值

type Student struct {
	Email *string // 使用指针是为了可以存空值
}

3.3 自动生成表结构

AutoMigrate 的逻辑是只新增,不删除,不修改(字段类型大小会修改)

DB.AutoMigrate(&Student{})   // 一个
DB.AutoMigrate(&Student{},&Teacher{})   // 多个

将结构体中的Name修改为Name1,进行迁移,会在表中多出一个name1的字段

type Student struct {
	ID    uint    // 默认使用ID作为主键
	Name  string
	Age   int
	Email *string // 使用指针是为了可以存空值
}

执行的SQL语句

ALTER TABLE `students` ADD `name1` longtext

3.4表名重定义(更具有描述性)

package main

import (
	"time"
)

type ArticleModel  struct {
	ID    uint
	Title string
    Tags  []TagModel `gorm:"many2many:article_tags;joinForeignKey:AriticleID;JoinReferences:TagID"` // many2many 用于反向查询,名字必须是主表_副表s,joinForeignKey 对应的是在中间表的主键字段,JoinReferences是所关联的对方主键ID
}

type TagModel struct {   // 如果表名为Tag,分不清到底是结构体还是数据库对象
	ID       uint
	Name     string
    Articles []ArticleModel `gorm:"many2many:article_tags;joinForeignKey:TagID;JoinReferences:AriticleID"`  // many2many 用于反向查询,名字必须是主表_副表s,joinForeignKey 对应的是在中间表的主键字段,JoinReferences是所关联的对方主键ID
}

type ArticleTagModel  struct {
	ArticleID   uint `gorm:"primarykey"` // 设置主键
	TagID       uint `gorm:"primarykey"` // 设置主键
	CreatedTime time.Time `json:"created_time"`
}

// 在写入数据库前给 CreatedTime 赋值
func (a *Article2Tag) BeforeCreate(db *gorm.DB) (err error) {
	a.CreatedTime = time.Now()
	return nil
}

func main() {
	// 设置Article表的Tags 表为Article2Tag
	DB.SetupJoinTable(&Article{}, "Tags", &Article2Tag{})
	DB.SetupJoinTable(&Tag{}, "Articles", &Article2Tag{})
	// 注意一定要记得迁移中间表--Article2Tag
	DB.AutoMigrate(&Article2Tag{},&Tag{},&Article2Tag{})

}

3.5字段约束属性

1. 通过标签来修改字段的属性

type Student struct {
	ID        uint `gorm:"size:10"`   
    Name      string `gorm:"size:6"` // 或者 `gorm:"type:varchar(6)"`
	Age       uint `gorm:"size:3"`
	MyStudent string `gorm:"size:128"`
}

2. 常用约束标签类型

多个标签之间用;连接

type 		   // 定义字段类型
size   		   // 定义字段大小
column         // 定义字段名称
primarykey     // 定义主键
unique     	   // 定义唯一键
default        // 定义默认值
not null       // 定义不可为空
embedded       // 定义嵌套字段
embeddedPrefix // 定义嵌套字段前缀
comment        // 定义注释

1. type

Name字段设置最大长度为6

type Student struct {
    Name     string `gorm:"type:varchar(6)"` 	// 直接写mysql的字段约束函数 
}

2. size

Name字段设置最大长度为6

type Student struct {
    Name     string `gorm:"size:6"` 	
}

3. column

定义数据库中生成的字段名

type Student struct {
    Name     string `gorm:"column:user_name"`  // 生成字段的时候会根据column的名字生成,注意,不要加在column:后面加空格 	
}

4. primarykey

定义数据库中的主键,默认以ID为主键

type Student struct {
    OrderNum     string `gorm:"primarykey"`  // 将订单编号设置为主键
}

5. unique

定义数据库中的唯一约束

type Student struct {
    OrderNum     string `gorm:"primarykey;unique"`  // 将订单编号设置为主键唯一
}

6. default

设置默认值

type Student struct {
    OrderNum     string `gorm:"primarykey;unique;default:2023110614323300001"`  // 将订单编号设置为主键唯一,并设置默认值
}

7. not nul

设置字段不允许为空

type Student struct {
    OrderNum     string `gorm:"primarykey;unique;not null;default:2023110614323300001"`  // 将订单编号设置为主键唯一,并设置默认值,及不允许为空
}

8. embedded

9. embeddedPrefix

10. comment

定义注释信息

type Student struct {
    OrderNum     string `gorm:"primarykey;unique;not null;default:2023110614323300001;comment:订单号"`  // 将订单编号设置为主键唯一,并设置默认值,及不允许为空,且注明注释信息
}

3.6 自定义数据类型

很多情况下我们存储到数据库中的数据是多变的,假如需要存储JSON或者是数组,很多数据库并不能直接存储这些数据类型,我们就需要自定义数据类型

自定义数据类型必须实现 ScannerValuer 接口,以便让Gorm知道如何将该类型接收,保存到数据库

3.6.1 存储JSON

存储JSON 到数据库的需求可能是经常会用到的,我们需要定义一个结构体,在入库的时候,将它转换成[]byte 类型,查询的时候将它转换回结构体

package main

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
)

type Info struct {
	Status string `json:"status"`
	Addr   string `json:"addr"`
	Age    int    `json:"age"`
}

// 1. 入库前转换为[]byte 类型
func (i *Info) Value() (driver.Value, error) {
	return json.Marshal(i)
}

// 2. 从数据库中取的时候转回结构体类型

func (i *Info) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("JSON反序列化为结构体失败,错误原因:", value))
	}
	err := json.Unmarshal(bytes, i)
	return err
}

type AuthModel struct {
	ID   uint
	Name string
	Info Info `gorm:"type:string"`
}

func main() {
	DB.AutoMigrate(&AuthModel{})
    DB.Create(&AuthModel{     // 调用Create方法 会自动执行Value(),会执行三次,一次是上面迁移的时候,一次是这里赋值的时候,一次是真正写入数据库前
		Name: "小明",
		Info: Info{
			Status: "Success",
			Addr:   "北京朝阳",
			Age:    18,
		},
	})
}

3.6.2 存储 数组

存储数组到数据库可能也经常会用到的,同样需要定义一个数组,在入库的时候,将它转换成[]byte 类型,查询的时候将它转换回结构体

package main

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
)

type Array []string

// 1. 入库前转换为[]byte 类型
func (a *Array) Value() (driver.Value, error) {
	return json.Marshal(a)
}

// 2. 从数据库中取的时候转回结构体类型

func (a *Array) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("转成byte数据错误,错误原因:", value))
	}
	err := json.Unmarshal(bytes, a)
	return err
}

type HostModel struct {
	ID   uint
	IP string
	Ports Array `gorm:"type:string"`
}

func main() {
	DB.AutoMigrate(&HostModel{})
	DB.Create(&HostModel{
		IP: "62.234.221.44",
		Ports: []string{"80","443"}
	})
}

当然也可以用字符串拼接的方式存储

package main

import (
	"database/sql/driver"
	"errors"
	"fmt"
	"strings"
)

type Array []string

// 1. 入库前转换为[]byte 类型
func (a *Array) Value() (driver.Value, error) {
	return strings.Join(a, "|"), nil
}

// 2. 从数据库中取的时候转回结构体类型

func (a *Array) Scan(value interface{}) error {
	data, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("转成byte数据错误,错误原因:", value))
	}
	*a = strings.Split(string(data), "|")
	return nil
}

type HostModel struct {
	ID    uint
	IP    string
	Ports Array `gorm:"type:string"`
}

func main() {
	DB.AutoMigrate(&HostModel{})
	DB.Create(&HostModel{
		IP:    "62.234.221.44",
		Ports: []string{"80", "443"}
	})
}

3.7 自定义枚举类型

很多时候会对一些状态进行判断,而这些状态的个数是有限的,例如:状态有;Running 运行中,OffLine 离线,Except 异常

如果存储字符串,不仅浪费空间,每次判断还要多复制很多字符,后期维护也麻烦,就可以用枚举来创建字段

package main

import (
	"encoding/json"
	"fmt"
)

// 定义类型别名
type Status int

const (
	Running Status = 1
	Except  Status = 2
	OffLine Status = 3
)

// 定义MarshalJSON,json反序列化的时候自动执行
func (s Status) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.String())
}

// 重写print相关方法,调用Print的时候显示中文
func (s Status) String() string {
	var str string
	switch s {
	case Running:
		str = "Running"
	case OffLine:
		str = "OffLine"
	case Except:
		str = "Except"
	}
	return str
}

type Host struct {
	ID     uint   `json:"id"`
	IP     string `json:"ip"`
	Status Status `gorm:"size:8" json:"status"`
}

func main() {
	// 插入数据
	DB.AutoMigrate(&Host{})
	DB.Create(&Host{
		IP:     "23.234.221.46",
		Status: Running,
	})

	// 查询
	var host Host
	DB.Take(&host, 1)
	data, _ := json.Marshal(&host)
	fmt.Println(string(data))

}

4.0 单表操作

4.0.0 表结构创建

type Student struct {
    ID    uint    // 默认使用ID作为主键
    Name  string
    Age   int
    Gende bool
    Email *string // 使用指针是为了可以存空值
}

4.0.1 插入

1. 单条插入

1. 所有字段都填写

func main() {

	DB.AutoMigrate(&Teacher{})
	email := "928394329@qq.com"
    // 1. 创建Teacher结构体对象
	wTeacher := &Teacher{     
		Name:   "王老师",
		Age:    34,
		Gender: true,
		Email:  &email,
	}
    // 2. 调用Create(),创建记录
    err := DB.Create(wTeacher).Error  // Create()会自动生成ID的值,由于wTeacher是一个指针,此时wTeacher的ID 就有值了
	if err != nil {
		fmt.Println("数据插入失败", err.Error())
	}
	fmt.Println("数据插入成功",wTeacher)       // 此时wTeacher的ID 就有值
}

2. 传空值,数据库保存为null

func main() {

	DB.AutoMigrate(&Teacher{})
    
    // 1. 创建Teacher结构体对象
	wTeacher := &Teacher{
		// Name:   "白老师",   Name不传,由于数据结构为string,默认值为空字符串,所以会在数据库保存为空字符串,而非 null
		Age:    37,
		Gender: true,    
         // Email:  &email,	不传Email,Email为空指针
        Email:nil,   // 或者传nil
	}
    
    // 2. 调用Create(),创建记录
	err := DB.Create(wTeacher).Error    // Create()会自动生成ID的值,由于wTeacher是一个指针,此时wTeacher的ID 就有值了
	if err != nil {
		fmt.Println("数据插入失败", err.Error())
	}
	fmt.Println("数据插入成功",wTeacher)    // 此时wTeacher的ID 就有值
}

2. 批量插入

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 声明一个切片
	var teachers []*Teacher

	// 2. 将所有老师对象添加到切片中
	for i := 0; i < 10; i++ {
		teachers = append(teachers, &Teacher{
			Name:   fmt.Sprintf("老师%d号", i+1),
			Age:    31 + i,
			Gender: true,
		})
	}
	// 3. 调用Create()方法,参数必须是指针类型,节省内存和性能
	err := DB.Create(&teachers).Error
	if err != nil {
		fmt.Println("数据插入失败", err.Error())
	}
	fmt.Println("数据插入成功")
}

4.0.2 查询

1. 第一条或最后一条

1. Take()

ORM 编译后执行的SQL

SELECT * FROM teachers LIMIT 1

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var cuTeacher Teacher
	err := DB.Take(&cuTeacher).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(cuTeacher)
}

2. First()

ORM 编译后执行的SQL

SELECT * FROM `teachers` ORDER BY `teachers`.`id` LIMIT 1

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var cuTeacher Teacher
	err := DB.First(&cuTeacher).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(cuTeacher)
}

3. Last()

ORM 编译后执行的SQL

SELECT * FROM `teachers` ORDER BY `teachers`.`id` DESC LIMIT 1

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var cuTeacher Teacher
    err := DB.Last(&cuTeacher).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(cuTeacher)
}

2. Take指定条件单条查询(查所有字段)

1. 指定ID查询

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` = 1 LIMIT 1

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var cuTeacher Teacher
    err := DB.Take(&cuTeacher,1).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(cuTeacher)
}

2. 指定Name查询

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE name = '王老师' LIMIT 1

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var cuTeacher Teacher
	err := DB.Take(&cuTeacher, "name = ?", "王老师").Error   // 这样写可以防止SQL 注入,不要自己拼接查询字符串,防止注入的原理是将特殊符号全部转义
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(cuTeacher)
}

3. 指定结构体查询

只能根据结构体中的ID主键查询

ORM 编译后执行的SQL

 SELECT * FROM `teachers` WHERE `teachers`.`id` = 2 LIMIT 1

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	cuTeacher := &Teacher{
		ID: 2,
	}
	err := DB.Take(cuTeacher).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(cuTeacher)
}

3. Find指定条件多条查询(查所有字段)

1. 不指定条件查所有

ORM 编译后执行的SQL

SELECT * FROM `teachers`

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var teachers []Teacher     // 定义为切片
    err := DB.Find(&teachers).Error   // 使用Find() 查询多条记录

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println(teachers)
}

2. 指定多 主键ID 查询

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (1,2)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var teachers []Teacher
	err := DB.Find(&teachers, []int{1, 2}).Error   // 查询ID为1和2的

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teachers)
	fmt.Println("序列化后:", string(data))
}

3. 指定多 Name 查询

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE name in ('王老师','白老师')

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var teachers []Teacher
	err := DB.Find(&teachers, "name in ?",[]string{"王老师","白老师"}).Error   // 查询Name 为王老师和白老师的

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teachers)
	fmt.Println("序列化后:", string(data))
}

4. Select() 筛选字段

1. 指定字段进行查询

DB.Find(&teacherList)     // select * from teacher
DB.Select("name","age").Find(&teacherList)   // select name,age from teacher
DB.Select([]string{"name","age"}).Find(&teacherList)   // select name,age from teacher

2. 配合Scan,将查询结果存入另一个结构体

type User struct{
    Name string
    Age  uint
}
var user User
DB.Select("name","age").Find(&teacherList).Scan(&user)
fmt.Println(user)

1. 上面方式会先查询一次teacher表,然后再根据条件查询,相当于查询了两次,消耗性能,解决方法:

// 方法一: 指定表名查询,缺点是必须知道数据库中的表名
type User struct{
    Name string
    Age  uint
}
var user User
DB.Table("teacher").Select("name","age").Scan(&user)
fmt.Println(user)

// 方法二: 指定结构体名,这样不用知道数据库中生成的真实表名
type User struct{
    Name string
    Age  uint
}
var user User
DB.Model(Teacher{}).Select("name","age").Scan(&user)
fmt.Println(user)

2. Scan 是根据列名扫描的

type User struct{
    Title string `gorm:"column:name"`    // 数据库中的列名为name,这里的查询列名会被翻译为title,所以需要添加column字段来指定列名
    Age  uint
}
var user User
DB.Table("teacher").Select("name","age").Scan(&user)
fmt.Println(user)

5. 条件查询

1. 准备数据

type Teacher struct {
	ID     uint   `gorm:size:3 `
	Name   string `gorm:size:8`
	Age    int
	Gender bool    `gorm:size:3`
	Email  *string `gorm:size:32`
}

func (t *Teacher) BeforeCreate(tx *gorm.DB) (err error) {
	email := fmt.Sprintf("%s@qq.com", t.Name)
	t.Email = &email
	return nil
}
func main() {
	var teacherList []Teacher
	teacherList = []Teacher{
		{ID: 1, Name: "李元芳", Age: 32, Gender: true},
		{ID: 2, Name: "张斌", Age: 18, Gender: true},
		{ID: 3, Name: "枫枫", Age: 24, Gender: true},
		{ID: 4, Name: "刘大", Age: 54, Gender: true},
		{ID: 5, Name: "李武", Age: 24, Gender: true},
		{ID: 6, Name: "李琦", Age: 14, Gender: false},
		{ID: 7, Name: "晓梅", Age: 25, Gender: false},
		{ID: 8, Name: "如燕", Age: 26, Gender: false},
		{ID: 9, Name: "魔灵", Age: 21, Gender: true},
	}
	err := DB.Create(&teacherList).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
}

2. Where()

1. 直接查询

查询用户名是枫枫的

var teacherList []Teacher
DB.Where("name = ?","枫枫").Find(&teacherList)

查询用户名不是枫枫的

var teacherList []Teacher
DB.Not("name = ?","枫枫").Find(&teacherList)
DB.Where("not name = ?","枫枫").Find(&teacherList)
DB.Where("name <> ?","枫枫").Find(&teacherList)

查询用户名包含如燕,李元芳的

var teacherList []Teacher
DB.Where("name in ?",[]string("如燕","李元芳")).Find(&teacherList)

查询姓李的

var teacherList []Teacher
DB.Where("name like ?","李%").Find(&teacherList)    // 只要是姓李的
DB.Where("name like ?","李_").Find(&teacherList)    // 姓李的两个字的
DB.Where("name like ?","李__").Find(&teacherList)   // 姓李的三个字的两个`_`下划线

查询年龄大于23,是qq邮箱的

var teacherList []Teacher
DB.Where("age > ? and email like ?",23,"%@qq.com").Find(&teacherList)
DB.Where("age > ?",23).Where("email like ?","%@qq.com").Find(&teacherList)

查询是qq邮箱的,或者是女的

var teacherList []Teacher
DB.Where("email like ? or gender = ?","%@qq.com",false).Find(&teacherList)
DB.Where("email like ?","%@qq.com").Or("gender = ?",false).Find(&teacherList)

2. 使用结构体查询

会过滤零值,并且结构体中的条件都是and关系

var teacherList []Teacher
DB.Where(&Teacher{
    Name:"李元芳",
    Age:0,    // 不会将age=0作为查询条件
}).Find(&teacherList)

3. 使用map查询

不会过滤零值,并且结构体中的条件都是and关系

var teacherList []Teacher
DB.Where(map[string]any{"name":"李元芳","age":0}).Find(&teacherList)  // 会将age=0作为查询条件

3. Not()

var teacherList []Teacher
DB.Not("name = ?","枫枫").Find(&teacherList)

4. Or()

DB.Where("email like ?","%@qq.com").Or("gender = ?",false).Find(&teacherList)

5. 排序

var users []Teacher
DB.Order("age asc").Find(&users)   // 降序
DB.Order("age asc").Find(&users)   // 升序

6. 分页

var users []Teacher
// 一页两条,第一页
DB.Limit(2).Offset(0).Find(&users)
// 第二页
DB.Limit(2).Offset(2).Find(&users)
// 第三页
DB.Limit(2).Offset(4).Find(&users)

通用写法

var users []Teacher
// 每页显示多少条
limit := 2

// 第几页
page := 1
offset := (page - 1) * limit
DB.Limit(limit).Offset(offset).Find(&users)

4.0.3 更新

1. Save() 单记录全字段更新

1. 会保存所有字段,即使该字段是零值也会保存

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` = 2 LIMIT 1
UPDATE `teachers` SET `name`='黄老师',`age`=0,`gender`=true,`email`=NULL WHERE `id` = 2

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为2的老师
	teacher := &Teacher{
		ID: 2,
	}
	DB.Take(teacher)

	// 2. 将该老师的名字改为 黄老师, 年龄改为 0
	teacher.Name = "黄老师"
    teacher.Age = 0

	// 3. 调用Save() 全字段保存
	err := DB.Save(teacher).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

2. 可以用select 选择更新指定字段

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` = 2 LIMIT 1
UPDATE `teachers` SET `name`='张老师',`age`=1 WHERE `id` = 2

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为2的老师
	teacher := &Teacher{
		ID: 2,
	}
	DB.Take(teacher)

	// 2. 将该老师的名字改为 黄老师
	teacher.Name = "张老师"
	teacher.Age = 1

	// 3. 调用Select() 指定只更新Name 和 Age 字段
	err := DB.Select("Name","Age").Save(teacher).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

2. Update() 多记录的单字段更新

1. 多条数据的单字段更新,会更新零值

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (1,2)
UPDATE `teachers` SET `age`=18 WHERE `teachers`.`id` IN (1,2) AND `id` IN (1,2)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为1和2的老师,将他们的年龄改为18,只修改一个字段
	var teacher []Teacher
	err := DB.Find(&teacher, []int{1, 2}).Update("age",0).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

2. 多条数据的多字段更新,会更新零值

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (1,2)
UPDATE `teachers` SET `age`=18 WHERE `teachers`.`id` IN (1,2) AND `id` IN (1,2)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为1和2的老师,将他们的年龄改为18,修改年龄后,再修改name
	var teacher []Teacher
	err := DB.Find(&teacher, []int{1, 2}).Update("age",0).Update("name","赵老师").Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

3. Updates() 多记录的单字段更新

1. 多条数据的多字段更新,默认不会更新零值,想要更新零值,需要使用Select()

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (1,2)
UPDATE `teachers` SET `age`=32 WHERE `teachers`.`id` IN (1,2) AND `id` IN (1,2)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为1和2的老师,根据结构体的字段修改
	var teacher []Teacher
	err := DB.Find(&teacher, []int{1, 2}).Updates(Teacher{
		Age: 32,
		Gender: false,    // 由于这里是false,所以Gender字段不会被更新
	}).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

2. 通过Select()指定字段,更新零值

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (1,2)
UPDATE `teachers` SET `age`=35,`gender`=false WHERE `teachers`.`id` IN (1,2) AND `id` IN (1,2)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为1和2的老师,根据结构体的字段修改
	var teacher []Teacher
	err := DB.Find(&teacher, []int{1, 2}).Select("age","gender").Updates(Teacher{
		Age: 35,
         Gender: false,   // 由于使用的是Select(),则零值也会被修改
	}).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

3. 使用map进行多字段更新

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (1,2)
UPDATE `teachers` SET `gender`=false,`name`='小黄' WHERE `teachers`.`id` IN (1,2) AND `id` IN (1,2)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为1和2的老师,根据map的字段修改
	var teacher []Teacher
	err := DB.Find(&teacher, []int{1, 2}).Updates(map[string]any{
		"name":"小黄",
		"gender":false,   // map更新会更新零值
	}).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

4.0.4 删除

1. 根据结构体删除

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` = 15
DELETE FROM `teachers` WHERE `teachers`.`id` = 15

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	// 1. 查找到ID为15的老师
	var teacher Teacher
	DB.Find(&teacher, 15)

	// 2. 删除ID为15的老师
	err := DB.Delete(&teacher).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

2. 指定 ID 删除多个

ORM 编译后执行的SQL

DELETE FROM `teachers` WHERE `teachers`.`id` IN (12,13,14)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var teacher Teacher

	// 删除ID为14,13,12的老师
	err := DB.Delete(&teacher, []int{14, 13, 12}).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teacher)
	fmt.Println("序列化后:", string(data))
}

3. 指定对象切片删除多个

ORM 编译后执行的SQL

SELECT * FROM `teachers` WHERE `teachers`.`id` IN (11,10,9)
DELETE FROM `teachers` WHERE `teachers`.`id` IN (9,10,11)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var teachers []Teacher

	// 1. 找到ID 为 11,10,9的老师对象
	DB.Find(&teachers, []int{11, 10, 9})
	
	// 2. 指定对象切片删除
	err := DB.Delete(&teachers).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	data, _ := json.Marshal(teachers)
	fmt.Println("序列化后:", string(data))
}

4. 删除所有

ORM 编译后执行的SQL

SELECT * FROM `teachers`
DELETE FROM `teachers` WHERE `teachers`.`id` IN (1,3,4,5,6,7,8)

ORM 查询

func main() {

	DB.AutoMigrate(&Teacher{})

	var teacherList []Teacher
	DB.Find(&teacherList).Delete(&teacherList)

	//switch err {
	//case nil:
	//case gorm.ErrRecordNotFound:
	//	fmt.Println("记录不存在")
	//default:
	//	fmt.Println("数据查询报错", err.Error())
	//}
}

4.1 一对多操作

4.1.0 表结构创建

外键名称是关联表名+ID,即 UserID

// 用户表  一个用户拥有多篇文章
type User struct {
	ID       uint   `gorm:"size:4"`
	Name     string `gorm:"size:8"`
	Articles []Article
}

// 文章表   一篇文章属于一个用户
type Article struct {
	ID     uint   `gorm:"size:4"`
	Title  string `gorm:"size:16"`
	UserID uint   `gorm:"size:4"` // foreign key 字段必须与表的类型完全一致,即,size也要完全一致
	User   User   // Go的继承字段
}

重写外键关联

指定gorm标签来重写外键名

// 用户表  一个用户拥有多篇文章
type User struct {
	ID       uint      `gorm:"size:4"`
	Name     string    `gorm:"size:8"`
	Articles []Article `gorm:"foreignKey:UID"`
}

// 文章表   一篇文章属于一个用户
type Article struct {
	ID     uint   `gorm:"size:4"`
	Title  string `gorm:"size:16"`
	UserID uint   `gorm:"size:4"` // foreign key 字段必须与表的类型完全一致,即,size也要完全一致
	User   User   `gorm:"foreignKey:UID"`
}

重写外键引用(基本不用这个)

// 用户表  一个用户拥有多篇文章
type User struct {
	ID       uint      `gorm:"size:4"`
	Name     string    `gorm:"size:8"`
    Articles []Article `gorm:"foreignKey:UserName;references:Name"`
}

// 文章表   一篇文章属于一个用户
type Article struct {
	ID        uint   `gorm:"size:4"`
	Title     string `gorm:"size:16"`
	UserName  string `gorm:"size:8"`
	User      User   `gorm:"references:Name"`
}

4.1.1 插入

1. 单条新增

1. 一表记录和多表记录同时创建

1.1 创建文章的同时创建用户(只有这种情况可以新增多条多表的记录)

SQL

INSERT INTO `articles` (`title`,`user_id`) VALUES ('python2',3),('Golang2',3) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

INSERT INTO `users` (`name`) VALUES ('枫枫')

Gorm

a1 := Article{Title: "Python"}
a2 := Article{Title: "Golang"}
user := User{
    Name:     "枫枫",
    Articles: []Article{a1, a2},
}

DB.create(&user)

1.2 创建用户的同时创建文章

SQL

INSERT INTO `users` (`name`) VALUES ('张三') ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `articles` (`title`,`user_id`) VALUES ('Golang高级',4)

Gorm

DB.Create(&Article{
	Title: "Golang高级",
	User: User{
		Name: "张三",
	},
})

2. 创建多表记录,并关联一表记录

查询到客户对象, 再添加关系

var user User
DB.Where("name = ?", "枫枫").Find(&user)
DB.Create(&Article{Title: "Python进阶",UserID: user.ID}) // 传ID
DB.Create(&Article{Title: "Python进阶",User: user})  // 传对象

2. HOOK(插入前的钩子函数)

type Teacher struct {
	ID     uint   `gorm:size:3 `
	Name   string `gorm:size:8`
	Age    int
	Gender bool    `gorm:size:3`
	Email  *string `gorm:size:32`
}

// 在每次真正插入到数据库前,都给这个对象赋值Email
func (t *Teacher) BeforeCreate(tx *gorm.DB) (err error) {
	email := fmt.Sprintf("%s@qq.com", t.Name)
	t.Email = &email
	return nil
}

4.1.2 更新

1. 无则新增,有则更新(Save())

1. 外键添加,给已有的用户绑定已有文章(更新)

SQL

INSERT INTO `articles` (`title`,`user_id`,`id`) VALUES ('',1,12) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

INSERT INTO `users` (`name`,`id`) VALUES ('枫枫',1) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`)

Gorm

// 1. 找到ID为1的用户
var user User
DB.Take(&user, 1)

// 2. 找到ID为5的文章,如果没有找到,此时的article中的所有字段都为空值,所以会创建一个新的字段为空值的article
var article Article
DB.Take(&article, 5)

// 3. 建立外键关系
user.Articles = []Article{article}
DB.Save(&user)

2. 由于在数据库中未匹配到数据时,默认是新增空值是没有意义的,所以需要加判断来避免添加空值

// 1. 找到ID为1的用户
var user User
userResult := DB.Take(&user, 1).RowsAffected

// 2. 找到ID为5的文章
var article Article
var err error
articleResult := DB.Take(&article, 18).RowsAffected

// 3. 判断两个是否在数据库匹配到数据了
if userResult != 0 && articleResult != 0 {
    // 3.1 如果匹配到了则建立关系
    err = DB.Model(&user).Association("Articles").Append(&article)
} else {
    // 3.2 没有匹配到就报错
    err = gorm.ErrRecordNotFound
}

3. 一次性新增多个

SQL

SELECT * FROM `users` WHERE `users`.`id` = 1 LIMIT 1

INSERT INTO `articles` (`title`,`user_id`) VALUES ('Golang',1),('Golang2',1) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

UPDATE `users` SET `name`='枫枫' WHERE `id` = 1

INSERT INTO `articles` (`title`,`user_id`,`id`) VALUES ('Golang',1,19),('Golang2',1,20) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

INSERT INTO `users` (`name`,`id`) VALUES ('枫枫',1) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`)

Gorm

// 1. 找到ID为1的用户
var user User
DB.Take(&user, 1)

// 2. 创建两个新的文章对象
a1 := Article{
    Title: "Golang",
}

a2 := Article{
    Title: "Golang2",
}

// 3. 建立关系
user.Articles = []Article{a1, a2}
DB.Save(&user)

2. 无则新增,有则更新 Append()

1. 外键添加,给已有的用户绑定已有文章(更新)

SQL

SELECT * FROM `users` WHERE `users`.`id` = 1 LIMIT 1

SELECT * FROM `articles` WHERE `articles`.`id` = 18 LIMIT 1

INSERT INTO `articles` (`title`,`user_id`) VALUES ('',1) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

Gorm

// 1. 找到ID为1的用户
var user User
DB.Take(&user, 1)

// 2. 找到ID为5的文章,如果没有找到,此时的article中的所有字段都为空值,所以会创建一个新的字段为空值的article
var article Article
DB.Take(&article, 5)

// 3. 建立关系
DB.Model(&user).Association("Articles").Append(&article)

2. 由于在数据库中未匹配到数据时,默认是新增空值是没有意义的,所以需要加判断来避免添加空值

// 1. 找到ID为1的用户
var user User
userResult := DB.Take(&user, 1).RowsAffected

// 2. 找到ID为5的文章
var article Article
var err error
articleResult := DB.Take(&article, 18).RowsAffected

// 3. 判断两个是否在数据库匹配到数据了
if userResult != 0 && articleResult != 0 {
    // 3.1 如果匹配到了则建立关系
    err = DB.Model(&user).Association("Articles").Append(&article)
} else {
    // 3.2 没有匹配到就报错
    err = gorm.ErrRecordNotFound
}

3. 一次性新增多个

SQL

SELECT * FROM `users` WHERE `users`.`id` = 1 LIMIT 1

INSERT INTO `articles` (`title`,`user_id`) VALUES ('Golang',1),('Golang2',1) ON DUPLICATE KEY UPDATE `user_id`=VALUES(`user_id`)

Gorm

// 1. 找到ID为1的用户
var user User
DB.Take(&user, 1)

// 2. 创建两个新的文章对象
a1 := Article{
    Title: "Golang",
}

a2 := Article{
    Title: "Golang2",
}

// 3. 建立关系
user.Articles = []Article{a1, a2}
DB.Save(&user)

4.1.2 查询

1. 预加载

1. 查询用户,显示用户的文章列表,像下面这样是查不出来的

var user User

DB.Take(&user,1)
fmt.Println(user)

2.所以必须要使用预加载来加载文章列表,预加载的名字就是外键关联的属性名

2.1 ID为1的作者写了哪些文章

SQL

SELECT * FROM `articles` WHERE `articles`.`user_id` = 1
SELECT * FROM `users` WHERE `users`.`id` = 1 LIMIT 1

Gorm

var user User

DB.Preload("Articles").Take(&user,1)

2.2 ID为1的文章是哪个作者写的

SQL

SELECT * FROM `users` WHERE `users`.`id` = 1
SELECT * FROM `articles` WHERE `articles`.`id` = 1 LIMIT 1

Gorm

var article Article

DB.Preload("User").Take(&article,1)

2. 嵌套预加载

以 点 连接外键关联的属性名

var user User

DB.Preload("Articles.User.Articles").Take(&user,1)

3. 带条件的预加载

只有 id > 1 的文章被查询出来

var user User

 DB.Preload("Articles","id > ?",2).Take(&user,1)

4. 自定义预加载

查询 ID为1的用户 写的id 为1和2的文章

DB.Preload("Articles", func(db *gorm.DB) *gorm.DB {
		return db.Where("id in ?",[]int{1,2})
	}).Take(&user,1)

4.1.3 删除

0. 指定删除

将id为1的作者删掉

var user User
DB.Model(&user).Delete(&user,1)

将id为1的用户删掉

var article Article
DB.Model(&article).Delete(&article,1)

1. 级联删除

删除用户的同时删除与用户关联的所有文章

DB.Take(&user,1)
	
DB.Select("Articles").Delete(&user)

2. 外键关系置NULL

删除用户的同时将与用户关联的所有文章外键设置为NULL

var user User

// 1. 找到id为2的用户
DB.Preload("Articles").Take(&user,2)

// 2. 将外键置为null
DB.Model(&user).Association("Articles").Delete(&user.Articles)

// 3. 删除用户
DB.Delete(&user)

4.2 一对一操作

经常用来拓展某一个表,常用的字段放在主表,不常用的放在详情表

4.2.0 表结构创建

type UserTwo struct {
	ID       uint   `gorm:"size:4"`
	Name     string `gorm:"size:8"`
	Age      int
	Gender   bool
	UserInfo *UserInfo // 通过本字段可以拿到用户详情信息
}

type UserInfo struct {
	UserTwoID uint `gorm:"size:4"` // 外键字段,必须是 表名+ID
	UserTwo *UserTwo
	ID        uint
	Addr      string
	Like      string
}

4.2.1 插入

1. 新增

SQL

INSERT INTO `user_infos` (`user_two_id`,`addr`,`like`) VALUES (1,'北京市朝阳区','狗') ON DUPLICATE KEY UPDATE `user_two_id`=VALUES(`user_two_id`)


INSERT INTO `user_twos` (`name`,`age`,`gender`) VALUES ('枫枫',18,true)

Gorm

DB.Create(&UserTwo{
		Name: "枫枫",
		Age: 18,
		Gender: true,
		UserInfo: &UserInfo{
			Addr: "北京市朝阳区",
			Like: "狗",
		},
	})

2. HOOK(插入前的钩子函数)

type Teacher struct {
	ID     uint   `gorm:size:3 `
	Name   string `gorm:size:8`
	Age    int
	Gender bool    `gorm:size:3`
	Email  *string `gorm:size:32`
}

// 在每次真正插入到数据库前,都给这个对象赋值Email
func (t *Teacher) BeforeCreate(tx *gorm.DB) (err error) {
	email := fmt.Sprintf("%s@qq.com", t.Name)
	t.Email = &email
	return nil
}

4.2.2. 更新(Save())

1. 外键添加,给已有的用户绑定已有文章(更新)

SQL

INSERT INTO `user_infos` (`user_two_id`,`addr`,`like`,`id`) VALUES (2,'北京大兴','唱跳rap',2) ON DUPLICATE KEY UPDATE `user_two_id`=VALUES(`user_two_id`)


INSERT INTO `user_twos` (`name`,`age`,`gender`,`id`) VALUES ('枫枫2',20,true,2) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`age`=VALUES(`age`),`gender`=VALUES(`gender`)

Gorm

// 1. 找到ID为1的用户
var user UserTwo
DB.Take(&user,2)


// 2. 建立外键关系
user.UserInfo = &UserInfo{
		Addr: "北京大兴",
		Like: "唱跳rap",
	}
DB.Save(&user)

2. 由于在数据库中未匹配到数据时,默认是新增空值是没有意义的,所以需要加判断来避免添加空值

// 1. 找到ID为1的用户
var user UserTwo
userResult := DB.Take(&user,2)

// 2. 找到ID为5的文章
var article Article
var err error
articleResult := DB.Take(&article, 18).RowsAffected

// 3. 判断两个是否在数据库匹配到数据了
if userResult != 0 && articleResult != 0 {
    // 3.1 如果匹配到了则建立关系
    err = DB.Model(&user).Association("Articles").Append(&article)
} else {
    // 3.2 没有匹配到就报错
    err = gorm.ErrRecordNotFound
}

4.2.3 查询

1. 预加载

1. 查询用户,显示用户的详情信息,像下面这样是查不出来的

var user User

DB.Take(&user,1)
fmt.Println(user)

2.所以必须要使用预加载来加载用户,预加载的名字就是外键关联的属性名

2.1 ID为1的用户的详情信息

SQL

SELECT * FROM `user_infos` WHERE `user_infos`.`user_two_id` = 1

SELECT * FROM `user_twos` WHERE `user_twos`.`id` = 1 LIMIT 1

Gorm

var user UserTwo

DB.Preload("UserInfo").Take(&user,1)

2.2 ID为1的用户详情是哪个用户

SQL

SELECT * FROM `user_twos` WHERE `user_twos`.`id` = 1

SELECT * FROM `user_infos` WHERE `user_infos`.`id` = 1 LIMIT 1

Gorm

var user UserInfo

DB.Preload("UserTwo").Take(&user,1)

4.2.4 删除

0. 指定删除

先删id为1的详情

var user UserInfo
DB.Model(&user).Delete(&user,1)

再删id为1的用户

var user UserTwo
DB.Model(&user).Delete(&user,1)

否则会因为有外键关系而不允许删除

1. 级联删除

删除用户的同时删除与用户关联的所有文章

var user UserInfo
DB.Take(&user,1)
	
DB.Select("UserInfo").Delete(&user)

2. 外键关系置NULL

删除用户的同时将与用户关联的所有文章外键设置为NULL

var user UserInfo

// 1. 找到id为2的用户
DB.Preload("UserInfo").Take(&user,2)

// 2. 将外键置为null
DB.Model(&user).Association("UserInfo").Delete(&user.Articles)

// 3. 删除用户
DB.Delete(&user)

4.3 多对多操作

4.3.0 表结构创建

type Tag struct {
	ID       uint `gorm:"size:4"`
	Name     string
	Articles []Article `gorm:"many2many:article_tags"` // 用于反向查询
}

type Article struct {
	ID    uint `gorm:"size:4"`
	Title string
	Tags  []Tag `gorm:"many2many:article_tags"`  // 用于反向查询
}

重写外键关联

指定gorm标签来重写外键名

// 用户表  一个用户拥有多篇文章
type User struct {
	ID       uint      `gorm:"size:4"`
	Name     string    `gorm:"size:8"`
	Articles []Article `gorm:"foreignKey:UID"`
}

// 文章表   一篇文章属于一个用户
type Article struct {
	ID     uint   `gorm:"size:4"`
	Title  string `gorm:"size:16"`
	UserID uint   `gorm:"size:4"` // foreign key 字段必须与表的类型完全一致,即,size也要完全一致
	User   User   `gorm:"foreignKey:UID"`
}

重写外键引用(基本不用这个)

// 用户表  一个用户拥有多篇文章
type User struct {
	ID       uint      `gorm:"size:4"`
	Name     string    `gorm:"size:8"`
    Articles []Article `gorm:"foreignKey:UserName;references:Name"`
}

// 文章表   一篇文章属于一个用户
type Article struct {
	ID        uint   `gorm:"size:4"`
	Title     string `gorm:"size:16"`
	UserName  string `gorm:"size:8"`
	User      User   `gorm:"references:Name"`
}

4.3.1 数据插入

1. 新增

1.1 两个表同时新增

创建名为盗墓笔记的文章,并给它绑定,恐怖和搞笑的标签

SQL

INSERT INTO `tags` (`name`) VALUES ('恐怖'),('搞笑') ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (27,4),(27,5) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

INSERT INTO `articles` (`title`) VALUES ('盗墓笔记')

Gorm

DB.Create(&Article{
    Title: "盗墓笔记",
    Tags: []Tag{
        {
            Name: "恐怖",
        },
        {
            Name: "搞笑",
        },
    },
})

创建惊悚标签,并绑定 捉鬼 和 道士下山

SQL

INSERT INTO `articles` (`title`) VALUES ('捉鬼'),('道士下山') ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`tag_id`,`article_id`) VALUES (12,30),(12,31) ON DUPLICATE KEY UPDATE `tag_id`=`tag_id`


INSERT INTO `tags` (`name`) VALUES ('惊悚')

Gorm

DB.Create(&Tag{
    Name: "惊悚",
    Articles: []Article{
        {
            Title: "捉鬼",
        },
        {
            Title: "道士下山",
        },
    },
})

1.2 新增一个后绑定数据库中已有的数据

新增一本叫鬼吹灯的文章,并从数据库中找到名字为恐怖的标签与之绑定

SQL

SELECT * FROM `tags` WHERE name = '恐怖'

INSERT INTO `tags` (`name`,`id`) VALUES ('恐怖',2) ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (26,2) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

INSERT INTO `articles` (`title`) VALUES ('鬼吹灯')

Gorm

var tags []Tag

DB.Find(&tags,"name = ?","恐怖")

DB.Create(&Article{
    Title: "鬼吹灯",
    Tags: tags,
})

新增一本叫鬼吹灯的文章,并从数据库中找到名字为恐怖和搞笑的标签与之绑定

SQL

SELECT * FROM `tags` WHERE name in ('恐怖','搞笑')

INSERT INTO `tags` (`name`,`id`) VALUES ('恐怖',2),('搞笑',3) ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (25,2),(25,3) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

INSERT INTO `articles` (`title`) VALUES ('鬼吹灯2')

Gorm

var tags []Tag

DB.Find(&tags,"name in ?",[]string{"恐怖","搞笑"})

DB.Create(&Article{
    Title: "鬼吹灯2",
    Tags: tags,
})

新增一本叫我的道士人生的文章,并从数据库中找到名字为恐怖和搞笑的标签与之绑定,并绑定新的都市标签

SQL

INSERT INTO `tags` (`name`) VALUES ('都市')

SELECT * FROM `tags` WHERE name in ('恐怖','搞笑','都市')

INSERT INTO `tags` (`name`,`id`) VALUES ('恐怖',2),('搞笑',3),('都市',11) ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (29,2),(29,3),(29,11) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

INSERT INTO `articles` (`title`) VALUES ('我的道士人生')

Gorm

var tags []Tag

DB.Create(&Tag{
    Name: "都市",
})

DB.Find(&tags, "name in ?", []string{"恐怖", "搞笑","都市"})

DB.Create(&Article{
    Title: "我的道士人生",
    Tags:  tags,
})

1.3 不新增文章,直接绑定数据库已有的标签

SQL

SELECT * FROM `articles` WHERE `articles`.`id` = 24
SELECT * FROM `tags` WHERE `tags`.`id` = 2
INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (24,2) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

Gorm

var article Article

// 找到文章id为24的文章
DB.Find(&article,24)

// 找到要绑定的标签对象
var tag Tag
DB.Find(&tag,2)
DB.Model(&article).Association("Tags").Append(&tag)

2. HOOK(插入前的钩子函数)

type Teacher struct {
	ID     uint   `gorm:size:3 `
	Name   string `gorm:size:8`
	Age    int
	Gender bool    `gorm:size:3`
	Email  *string `gorm:size:32`
}

// 在每次真正插入到数据库前,都给这个对象赋值Email
func (t *Teacher) BeforeCreate(tx *gorm.DB) (err error) {
	email := fmt.Sprintf("%s@qq.com", t.Name)
	t.Email = &email
	return nil
}

4.3.2 查询

1. 预加载

1. 查询文章,显示文章的标签列表,像下面这样是查不出来的

var article Article

DB.Take(&article,1)
fmt.Println(article)

2.所以必须要使用预加载来加载标签列表,预加载的名字就是外键关联的属性名

2.1 Article 的 ID为23的文章都有哪些标签

SQL

SELECT * FROM `article_tags` WHERE `article_tags`.`article_id` = 23
SELECT * FROM `tags` WHERE `tags`.`id` IN (2,3)

Gorm

var article Article
DB.Preload("Tags").Find(&article,23)

2.2 Tag 的 ID为2的标签都有哪些文章

SQL

SELECT * FROM `article_tags` WHERE `article_tags`.`tag_id` = 2
SELECT * FROM `articles` WHERE `articles`.`id` IN (23,24,25,26,29)
SELECT * FROM `tags` WHERE `tags`.`id` = 2

Gorm

var tag Tag
DB.Preload("Articles").Find(&tag,2)

2. 带条件的预加载

只有 id > 24 的标签被查询出来

var tag Tag
DB.Preload("Articles","id > ?",24).Find(&tag,2)

3. 自定义预加载

查询 Tag ID为2的标签 Ariticle ID为23,25的文章

var tag Tag
DB.Preload("Articles", func(db *gorm.DB) *gorm.DB {
    return db.Where("id in ?",[]int{23,25})
}).Find(&tag,2)

4.3.3 更新

1.先删除原有的再添加新的

SQL

SELECT * FROM `article_tags` WHERE `article_tags`.`article_id` = 24
SELECT * FROM `articles` WHERE `articles`.`id` = 24

DELETE FROM `article_tags` WHERE `article_tags`.`article_id` = 24 AND `article_tags`.`tag_id` IN (NULL)

SELECT * FROM `tags` WHERE `tags`.`id` = 2

INSERT INTO `tags` (`name`,`id`) VALUES ('恐怖',2) ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (24,2) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

Gorm

var article Article

// 删除文章id为24的所有标签
DB.Preload("Tags").Find(&article,24)
DB.Model(&article).Association("Tags").Delete(article.Tags)

// 找到绑定的标签对象
var tag Tag
DB.Find(&tag,2)
DB.Model(&article).Association("Tags").Append(&tag)

2. 调用Replace() 进行替换

SQL

SELECT * FROM `article_tags` WHERE `article_tags`.`article_id` = 24

SELECT * FROM `tags` WHERE `tags`.`id` IN (3,12)

SELECT * FROM `articles` WHERE id = 24

SELECT * FROM `tags` WHERE `tags`.`id` = 2 LIMIT 1

INSERT INTO `tags` (`name`,`id`) VALUES ('恐怖',2) ON DUPLICATE KEY UPDATE `id`=`id`

INSERT INTO `article_tags` (`article_id`,`tag_id`) VALUES (24,2) ON DUPLICATE KEY UPDATE `article_id`=`article_id`

DELETE FROM `article_tags` WHERE `article_tags`.`article_id` = 24 AND `article_tags`.`tag_id` <> 2

Gorm

var article Article

// 找到ID为24的文章对象
DB.Preload("Tags").Find(&article, "id = ?", 24)

// 找到ID为2的标签对象
var tag Tag
DB.Take(&tag,2)
// 把文章ID为24的记录所对应的标签对象替换为新的标签对象
DB.Model(&article).Association("Tags").Replace(&tag)

4.3.4 删除

0. 指定删除

SQL

SELECT * FROM `article_tags` WHERE `article_tags`.`article_id` = 24
SELECT * FROM `articles` WHERE `articles`.`id` = 24

DELETE FROM `article_tags` WHERE `article_tags`.`article_id` = 24 AND `article_tags`.`tag_id` IN (NULL)

Gorm

var article Article

// 删除文章id为24的所有标签
DB.Preload("Tags").Find(&article,24)
DB.Model(&article).Association("Tags").Delete(article.Tags)

1. 级联删除

删除用户的同时删除与用户关联的所有文章

DB.Take(&user,1)
	
DB.Select("Articles").Delete(&user)

2. 外键关系置NULL

删除用户的同时将与用户关联的所有文章外键设置为NULL

var user User

// 1. 找到id为2的用户
DB.Preload("Articles").Take(&user,2)

// 2. 将外键置为null
DB.Model(&user).Association("Articles").Delete(&user.Articles)

// 3. 删除用户
DB.Delete(&user)

4.3.5 自定义中间表

0. 表结构创建

1. 如果在之前创建了默认的多对多关系表,要先将原表全部删除

默认的中间表只有双方的主键ID,展示不了更多信息,可以自定义中间表来实现在中间表中,增加更多字段

package main

import (
	"time"
)

type Tag struct {
	ID       uint
	Name     string
	Articles []Article `gorm:"many2many:article_tags;"`  // 用于反向查询,名字必须是主表_副表s
}

type Article struct {
	ID    uint
	Title string
	Tags  []Tag `gorm:"many2many:article_tags;"` // 用于反向查询
}

type ArticleTag struct {
	ArticleID   uint `gorm:"primarykey"` // 设置主键
	TagID       uint `gorm:"primarykey"` // 设置主键
	CreatedTime time.Time `json:"created_time"`
}

// 在写入数据库前给 CreatedTime 赋值
func (a *Article2Tag) BeforeCreate(db *gorm.DB) (err error) {
	a.CreatedTime = time.Now()
	return nil
}

func main() {
	// 设置Article表的Tags 表为Article2Tag
	DB.SetupJoinTable(&Article{}, "Tags", &Article2Tag{})
	DB.SetupJoinTable(&Tag{}, "Articles", &Article2Tag{})
	// 注意一定要记得迁移中间表--Article2Tag
	DB.AutoMigrate(&Article2Tag{},&Tag{},&Article2Tag{})

}

1. 中间表添加外键关系

1. 操作中间表给文章ID为3的添加Tag1,Tag2

DB.Create(&[]Article2Tag{
    {
        ArticleID: 3,
        TagID:     1,
        CreatedTime: time.Now(),
    },
    {
        ArticleID: 3,
        TagID:     2,
        CreatedTime: time.Now(),
    },
})

2. 添加新文章,关联已有Tag

// 找到ID为1,2的标签对象
var tags []Tag
DB.Find(&tags,[]int{1,2})

DB.Create(&Article{
    Title: "Django",
    Tags: tags,
})

2. 修改

1. 给已有文章替换新的Tag

// 找到需要替换的文章对象
var article Article
DB.Preload("Tags").Take(&article,2)

// 找到新的标签对象
var tags []Tag
DB.Find(&tags,[]int{1,3})
DB.Model(&article).Assciation("Tags").Replace(&tags)

3. 查询

1. 直接查询不好做分页,且拿不到标签创建的时间

var article Article
// 1. 找到ID为2 的文章的所有标签
DB.Preload("Tags").Take(&article,2)

如果想做分页只能用Association,但是这样写比较麻烦,

var article Article
DB.Preload("Tags").Take(&article,2)

var tags []Tag
DB.Model(&article).limit(1).Association("Articles").Find(&tags)

2. 比较简单的方法就是直接操作中间表,既可以做分页,也可以拿到标签创建时间,但是这种方式只能根据用户ID查询,且查不到文章和标签的其他字段

var articleTags []ArticleTag
DB.limit(1).Find(&articleTags,1)

如果想在中间表查到文章和标签的其他信息,就要修改表结构添加外键关联到文章和标签

// 中间表中添加外键关联到用户表
type ArticleTag struct {
	ArticleID   uint        `gorm:"primarykey"`           // 设置主键
	Article	    Article 	`gorm:"foreignKey:ArticleID"` // 外键关联主键
	TagID       uint        `gorm:"primarykey"`           // 设置主键
	Tag		    Tag		    `gorm:"foreignKey:TagID"`     // 外键关联主键
	CreatedTime time.Time   `json:"created_time"`
}

然后再查询就要用Preload来预加载文章和标签的其他字段了

var articleTags []ArticleTag
DB.Preload("Article").Preload("Tag").limit(1).Find(&articleTags,1)

5. 全局自定义钩子(Hook)

5.0 生命周期

Hook 是在创建、查询、更新、删除等操作之前、之后调用的函数。

如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。如果任何回调返回错误,GORM 将停止后续的操作并回滚事务。

钩子方法的函数签名应该是 func(*gorm.DB) error

5.1 创建

注意GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚

5.1.0 执行顺序

// 开始事务
BeforeSave
BeforeCreate
// 关联前的 save
// 插入记录至 db
// 关联后的 save
AfterCreate
AfterSave
// 提交或回滚事务

5.1.1 BeforeSave()

func (t *Teacher) BeforeSave(tx *gorm.DB) (err error) {
	email := fmt.Sprintf("%s@qq.com", t.Name)
	t.Email = &email
	return nil
}

5.1.2 BeforeCreate()

注意GORM 中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

type Teacher struct {
	ID     uint   `gorm:size:3 `
	Name   string `gorm:size:8`
	Age    int
	Gender bool    `gorm:size:3`
	Email  *string `gorm:size:32`
}

// 在每次真正插入到数据库前,都给这个对象赋值Email
func (t *Teacher) BeforeCreate(tx *gorm.DB) (err error) {
	email := fmt.Sprintf("%s@qq.com", t.Name)
	t.Email = &email
	return nil
}

5.1.3 AfterCreate()

注意GORM中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

func (t *Teacher) AfterCreate(tx *gorm.DB) (err error) {
	if t.ID == 1 {
		tx.Model(u).Update("role", "admin")
	}
    if !u.IsValid() {
		return errors.New("rollback invalid user")
    }
	return
}

5.1.4 AfterSave()

注意GORM中保存、删除操作会默认运行在事务上, 因此在事务完成之前该事务中所作的更改是不可见的,如果您的钩子返回了任何错误,则修改将被回滚。

func (t *Teacher) AfterSave(tx *gorm.DB) (err error) {
	if t.ID == 1 {
		tx.Model(u).Update("role", "admin")
	}
    if !u.IsValid() {
		return errors.New("rollback invalid user")
    }
	return
}

5.2 更新

5.2.0 执行顺序

// 开始事务
BeforeSave
BeforeUpdate
// 关联前的 save
// 更新 db
// 关联后的 save
AfterUpdate
AfterSave
// 提交或回滚事务

5.2.1 BeforeSave()

func (u *User) BeforeSave(tx *gorm.DB) (err error) {
  if u.readonly() {
    err = errors.New("read only user")
  }
  return
}


5.2.2 BeforeUpdate()

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
  if u.readonly() {
    err = errors.New("read only user")
  }
  return
}


5.2.3 AfterUpdate()

// 在同一个事务中更新数据
func (u *User) AfterUpdate(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
  }
  return
}

5.2.4 AfterSave()

// 在同一个事务中更新数据
func (u *User) AfterSave(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("verfied", true)
  }
  return
}

5.3 删除

5.2.0 执行顺序

// 开始事务
BeforeDelete
// 删除 db 中的数据
AfterDelete
// 提交或回滚事务

5.2.1 BeforeDelete()

// 在同一个事务中更新数据
func (u *User) BeforeDelete(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
  }
  return
}

5.2.1 AfterDelete()

// 在同一个事务中更新数据
func (u *User) AfterDelete(tx *gorm.DB) (err error) {
  if u.Confirmed {
    tx.Model(&Address{}).Where("user_id = ?", u.ID).Update("invalid", false)
  }
  return
}

5.4 查询

5.2.0 执行顺序

// 从 db 中加载数据
// Preloading (eager loading)
AfterFind

5.2.1 BeforeUpdate()

func (u *User) AfterFind(tx *gorm.DB) (err error) {
  if u.MemberShip == "" {
    u.MemberShip = "user"
  }
  return
}

5.5 修改当前操作

func (u *User) BeforeCreate(tx *gorm.DB) error {
  // 通过 tx.Statement 修改当前操作,例如:
  tx.Statement.Select("Name", "Age")
  tx.Statement.AddClause(clause.OnConflict{DoNothing: true})

  // tx 是带有 `NewDB` 选项的新会话模式 
  // 基于 tx 的操作会在同一个事务中,但不会带上任何当前的条件
  err := tx.First(&role, "name = ?", user.Role).Error
  // SELECT * FROM roles WHERE name = "admin"
  // ...
  return err
}

6. 常用公共属性

7.5.1 获取查询条数及异常判断

1. 获取查询的记录数

DB.Take(cuTeacher).RowsAffected
DB.Find(cuTeacher).RowsAffected
DB.Save(cuTeacher).RowsAffected

2. 是否查询失败

DB.Take(cuTeacher).Error
DB.Find(cuTeacher).Error

3. 异常判断

查询失败有查询为空,查询条件错误,SQL语法错误,可以使用switch进行判断

	err := DB.Take(cuTeacher).Error
	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}

7.5.2 结构体序列化显示字段键值

由于Email 是指针类型,所以看不到实际内容,但是序列化后,会显示内容

type Teacher struct {
    ID     uint   `gorm:size:3 json:"id"`   // 指定json序列化的名称
	Name   string `gorm:size:8`
	Age    int
	Gender bool    `gorm:size:3`
	Email  *string `gorm:size:32`
}

func main() {

	DB.AutoMigrate(&Teacher{})

	var teachers []Teacher
	err := DB.Find(&teachers).Error

	switch err {
	case nil:
	case gorm.ErrRecordNotFound:
		fmt.Println("记录不存在")
	default:
		fmt.Println("数据查询报错", err.Error())
	}
	fmt.Println("序列化前:",teachers)
	for _, teacher := range teachers {
		data,_ := json.Marshal(teacher)
		fmt.Println("序列化后:",string(data))
	}
}

// 打印结果
序列化前: [{1 王老师 34 false 0xc0002091f0} {2  37 true <nil>}]
序列化后: {"ID":1,"Name":"王老师","Age":34,"Gender":false,"Email":"928394329@qq.com"}
序列化后: {"ID":2,"Name":"","Age":37,"Gender":true,"Email":null}

7. 事务

事务就是用户定义的一系列数据库操作,这些操作可以视为一个完成的逻辑处理工作单元,要么全部执行,要么全部不执行,是不可分割的工作单元。

很形象的一个例子,张三给李四转账100元,在程序里面,张三的余额就要-100,李四的余额就要+100
整个事件是一个整体,哪一步错了,整个事件都是失败的

gorm事务默认是开启的。为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。

如果没有这方面的要求,您可以在初始化时禁用它,这将获得大约 30%+ 性能提升。

7.6.1 禁用全局事务(不推荐)

// 全局禁用
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  SkipDefaultTransaction: true,
})

7.6.2 测试模型创建

type User struct {
  ID    uint   `json:"id"`
  Name  string `json:"name"`
  Money int    `json:"money"`
}

// InnoDB引擎才支持事务,MyISAM不支持事务
// DB.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})

7.6.3 不开启事务的后果

以张三给李四转账为例,不使用事务的后果

var zhangsan, lisi User
DB.Take(&zhangsan, "name = ?", "张三")
DB.Take(&lisi, "name = ?", "李四")
// 张三给李四转账100元
// 先给张三-100
zhangsan.Money -= 100
DB.Model(&zhangsan).Update("money", zhangsan.Money)
// 这里模拟失败的情况

// 再给李四+100
lisi.Money += 100
DB.Model(&lisi).Update("money", lisi.Money)

在失败的情况下,要么张三白白损失了100,要么李四凭空拿到100元这显然是不合逻辑的,并且不合法的

7.6.3 自动事务

func main() {
	DB.AutoMigrate(&User{})

	var zhangsan, lisi User

	DB.Take(&zhangsan, "name=?", "zhangsan")
	DB.Take(&lisi, "name=?", "lisi")

	fmt.Println(zhangsan)
	fmt.Println(lisi)
	// 开启事务
	DB.Transaction(func(tx *gorm.DB) error {
		zhangsan.Money -= 100
         // 注意事务中的所有数据库操作都需要使用tx来执行
		err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
		if err != nil {
			fmt.Println(err)
			return err
		}
		lisi.Money += 100
		err = tx.Model(&lisi).Update("money", lisi.Money).Error
		if err != nil {
			fmt.Println(err)
			return err
		}
		// 在函数返回前,如果没有报错会自动提交事务
		return nil
	})

}

使用事务之后,他们就是一体,一起成功,一起失败

7.6.4 手动事务

// 开始事务
tx := db.Begin()

// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
tx.Create(...)

// ...

// 遇到错误时回滚事务
tx.Rollback()

// 否则,提交事务
tx.Commit()

刚才的代码也可以这样实现

func main() {
	DB.AutoMigrate(&User{})

	var zhangsan, lisi User

	DB.Take(&zhangsan, "name=?", "zhangsan")
	DB.Take(&lisi, "name=?", "lisi")

	fmt.Println(zhangsan)
	fmt.Println(lisi)

	tx := DB.Begin()

	// 先给张三-100
	zhangsan.Money -= 100
	err := tx.Model(&zhangsan).Update("money", zhangsan.Money).Error
	if err != nil {
		tx.Rollback()
	}

	// 再给李四+100
	lisi.Money += 100
	err = tx.Model(&lisi).Update("money", lisi.Money).Error
	if err != nil {
		tx.Rollback()
	}
	// 提交事务
	tx.Commit()

}

8. 连接池

因为TCP的三只握手等等原因,建立一个连接是一件成本比较高的行为。所以在一个需要多次与特定实体交互的程序中,就需要维持一个连接池,里面有可以复用的连接可供重复使用。

而维持一个连接池,最基本的要求就是要做到:thread safe(线程安全),尤其是在Golang这种特性是goroutine的语言中。

8.1 实现简单的连接池

1. 创建连接池类

type Pool struct {
	m       sync.Mutex                // 保证多个goroutine访问时候,closed的线程安全
	res     chan io.Closer            // 连接存储的chan
	factory func() (io.Closer, error) // 新建连接的工厂方法
	closed  bool                      // 连接池关闭标志
}

这个简单的连接池,我们利用 chan 来存储池里的连接。而新建结构体的方法也比较简单

func New(fn func() (io.Closer, error), size uint) (*Pool, error) {
    if size <= 0 {
        return nil, errors.New("size的值太小了。")
    }
    return &Pool{
        factory: fn,
        res:     make(chan io.Closer, size),
    }, nil
}

只需要提供对应的工厂函数和连接池的大小就可以了。

2. 获取连接

那么我们要怎么从中获取资源呢?因为我们内部存储连接的结构是chan,所以只需要简单的 select 就可以保证线程安全:

//从资源池里获取一个资源
func (p *Pool) Acquire() (io.Closer,error) {
    select {
    case r,ok := <-p.res:
        log.Println("Acquire:共享资源")
        if !ok {
            return nil,ErrPoolClosed
        }
        return r,nil
    default:
        log.Println("Acquire:新生成资源")
        return p.factory()
    }
}

我们先从连接池的res这个chan里面获取,如果没有的话我们就利用我们早已经准备好的工厂函数进行构造连接。同时我们在从res获取连接的时候利用**ok**先确定了这个连接池是否已经关闭。如果已经关闭的话我们就返回早已经准备好的连接已关闭错误。

3. 关闭连接池

/关闭资源池,释放资源
func (p *Pool) Close() {
    p.m.Lock()
    defer p.m.Unlock()

    if p.closed {
        return
    }

    p.closed = true

    //关闭通道,不让写入了
    close(p.res)

    //关闭通道里的资源
    for r:=range p.res {
        r.Close()
    }
}

这边我们需要先进行 p.m.Lock() 上锁操作,这么做是因为我们需要对结构体里面的 closed 进行读写。需要先把这个标志位设定后,关闭res这个chan,使得 Acquire 方法无法再获取新的连接。我们再对 res 这个 chan 里面的连接进行 Close 操作。

4. 释放连接

释放连接首先得有个前提,就是连接池还没有关闭。如果连接池已经关闭再往**res**里面送连接的话就好触发panic

func (p *Pool) Release(r io.Closer){
    //保证该操作和Close方法的操作是安全的
    p.m.Lock()
    defer p.m.Unlock()

    //资源池都关闭了,就省这一个没有释放的资源了,释放即可
    if p.closed {
        r.Close()
        return
    }

    select {
    case p.res <- r:
        log.Println("资源释放到池子里了")
    default:
        log.Println("资源池满了,释放这个资源吧")
        r.Close()
    }
}

5. 以上就是一个简单且线程安全的连接池实现方式了。我们可以看到的是,现在连接池虽然已经实现了,但是还有几个小缺点:

  1. 我们对连接最大的数量没有限制,如果线程池空的话都我们默认就直接新建一个连接返回了。一旦并发量高的话将会不断新建连接,很容易(尤其是MySQL)造成too many connections的报错发生。
  2. 既然我们需要保证最大可获取连接数量,那么我们就不希望数量定的太死。希望空闲的时候可以维护一定的空闲连接数量idleNum,但是又希望我们能限制最大可获取连接数量maxNum。
  3. 第一种情况是并发过多的情况,那么如果并发量过少呢?现在我们在新建一个连接并且归还后,我们很长一段时间不再使用这个连接。那么这个连接很有可能在几个小时甚至更长时间之前就已经建立的了。长时间闲置的连接我们并没有办法保证它的可用性。便有可能我们下次获取的连接是已经失效的连接。

8.2 标准库的SQL连接池

Golang的连接池实现在标准库 database/sql/sql.go 下。当执行 sql.open() 的时候,就会打开一个连接池。

db, err := sql.Open("mysql", "xxxx")

我们可以看看返回的 db 的结构体

type DB struct {
    waitDuration int64 // Total time waited for new connections.
    mu           sync.Mutex // protects following fields
    freeConn     []*driverConn
    connRequests map[uint64]chan connRequest
    nextRequest  uint64 // Next key to use in connRequests.
    numOpen      int    // number of opened and pending open connections
    // Used to signal the need for new connections
    // a goroutine running connectionOpener() reads on this chan and
    // maybeOpenNewConnections sends on the chan (one send per needed connection)
    // It is closed during db.Close(). The close tells the connectionOpener
    // goroutine to exit.
    openerCh          chan struct{}
    closed            bool
    maxIdle           int                    // zero means defaultMaxIdleConns; negative means 0
    maxOpen           int                    // <= 0 means unlimited
    maxLifetime       time.Duration          // maximum amount of time a connection may be reused
    cleanerCh         chan struct{}
    waitCount         int64 // Total number of connections waited for.
    maxIdleClosed     int64 // Total number of connections closed due to idle.
    maxLifetimeClosed int64 // Total number of connections closed due to max free limit.
}

上面省去了一些暂时不需要关注的field。我们可以看的,DB这个连接池内部存储连接的结构 freeConn ,并不是我们之前使用的chan,而是 []driverConn ,一个连接切片。同时我们还可以看到,里面有 maxIdle 等相关变量来控制空闲连接数量。值得注意的是, DB 的初始化函数 Open 函数并没有新建数据库连接。而新建连接在哪个函数呢?我们可以在 Query 方法一路往回找,我们可以看到这个函数: func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) 。而我们从连接池获取连接的方法,就从这里开始:

1. 获取连接

// conn returns a newly-opened or cached *driverConn.
func (db *DB) conn(ctx context.Context, strategy connReuseStrategy) (*driverConn, error) {
    // 先判断db是否已经关闭。
    db.mu.Lock()
    if db.closed {
        db.mu.Unlock()
        return nil, errDBClosed
    }
    // 注意检测context是否已经被超时等原因被取消。
    select {
    default:
    case <-ctx.Done():
        db.mu.Unlock()
        return nil, ctx.Err()
    }
    lifetime := db.maxLifetime

    // 这边如果在freeConn这个切片有空闲连接的话,就left pop一个出列。注意的是,这边因为是切片操作,所以需要前面需要加锁且获取后进行解锁操作。同时判断返回的连接是否已经过期。
    numFree := len(db.freeConn)
    if strategy == cachedOrNewConn && numFree > 0 {
        conn := db.freeConn[0]
        copy(db.freeConn, db.freeConn[1:])
        db.freeConn = db.freeConn[:numFree-1]
        conn.inUse = true
        db.mu.Unlock()
        if conn.expired(lifetime) {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        // Lock around reading lastErr to ensure the session resetter finished.
        conn.Lock()
        err := conn.lastErr
        conn.Unlock()
        if err == driver.ErrBadConn {
            conn.Close()
            return nil, driver.ErrBadConn
        }
        return conn, nil
    }

    // 这边就是等候获取连接的重点了。当空闲的连接为空的时候,这边将会新建一个request(的等待连接 的请求)并且开始等待
    if db.maxOpen > 0 && db.numOpen >= db.maxOpen {
        // 下面的动作相当于往connRequests这个map插入自己的号码牌。
        // 插入号码牌之后这边就不需要阻塞等待继续往下走逻辑。
        req := make(chan connRequest, 1)
        reqKey := db.nextRequestKeyLocked()
        db.connRequests[reqKey] = req
        db.waitCount++
        db.mu.Unlock()

        waitStart := time.Now()

        // Timeout the connection request with the context.
        select {
        case <-ctx.Done():
            // context取消操作的时候,记得从connRequests这个map取走自己的号码牌。
            db.mu.Lock()
            delete(db.connRequests, reqKey)
            db.mu.Unlock()

            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

            select {
            default:
            case ret, ok := <-req:
                // 这边值得注意了,因为现在已经被context取消了。但是刚刚放了自己的号码牌进去排队里面。意思是说不定已经发了连接了,所以得注意归还!
                if ok && ret.conn != nil {
                    db.putConn(ret.conn, ret.err, false)
                }
            }
            return nil, ctx.Err()
        case ret, ok := <-req:
            // 下面是已经获得连接后的操作了。检测一下获得连接的状况。因为有可能已经过期了等等。
            atomic.AddInt64(&db.waitDuration, int64(time.Since(waitStart)))

            if !ok {
                return nil, errDBClosed
            }
            if ret.err == nil && ret.conn.expired(lifetime) {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            if ret.conn == nil {
                return nil, ret.err
            }
            ret.conn.Lock()
            err := ret.conn.lastErr
            ret.conn.Unlock()
            if err == driver.ErrBadConn {
                ret.conn.Close()
                return nil, driver.ErrBadConn
            }
            return ret.conn, ret.err
        }
    }
    // 下面就是如果上面说的限制情况不存在,可以创建先连接时候,要做的创建连接操作了。
    db.numOpen++ // optimistically
    db.mu.Unlock()
    ci, err := db.connector.Connect(ctx)
    if err != nil {
        db.mu.Lock()
        db.numOpen-- // correct for earlier optimism
        db.maybeOpenNewConnections()
        db.mu.Unlock()
        return nil, err
    }
    db.mu.Lock()
    dc := &driverConn{
        db:        db,
        createdAt: nowFunc(),
        ci:        ci,
        inUse:     true,
    }
    db.addDepLocked(dc, dc)
    db.mu.Unlock()
    return dc, nil
}

简单来说, DB 结构体除了用的是 slice 来存储连接,还加了一个类似排队机制的 connRequest 来解决获取等待连接的过程。同时在判断连接健康性都有很好的兼顾。那么既然有了排队机制,归还连接的时候是怎么做的呢?

2. 释放连接

我们可以直接找到func (db *DB) putConnDBLocked(dc *driverConn, err error) bool这个方法。就像注释说的,这个方法主要的目的是:

Satisfy a connRequest or put the driverConn in the idle pool and return true or return false.

我们主要来看看里面重点那几行:

...
    // 如果已经超过最大打开数量了,就不需要在回归pool了
    if db.maxOpen > 0 && db.numOpen > db.maxOpen {
        return false
    }
    // 这边是重点了,基本来说就是从connRequest这个map里面随机抽一个在排队等着的请求。取出来后发给他。就不用归还池子了。
    if c := len(db.connRequests); c > 0 {
        var req chan connRequest
        var reqKey uint64
        for reqKey, req = range db.connRequests {
            break
        }
        delete(db.connRequests, reqKey) // 删除这个在排队的请求。
        if err == nil {
            dc.inUse = true
        }
        // 把连接给这个正在排队的连接。
        req <- connRequest{
            conn: dc,
            err:  err,
        }
        return true
    } else if err == nil && !db.closed {
        // 既然没人排队,就看看到了最大连接数目没有。没到就归还给freeConn。
        if db.maxIdleConnsLocked() > len(db.freeConn) {
            db.freeConn = append(db.freeConn, dc)
            db.startCleanerLocked()
            return true
        }
        db.maxIdleClosed++
    }
...

我们可以看到,当归还连接时候,如果有在排队轮候的请求就不归还给池子直接发给在轮候的人了。

现在基本就解决前面说的小问题了。不会出现连接太多导致无法控制too many connections的情况。也很好了维持了连接池的最小数量。同时也做了相关对于连接健康性的检查操作。
值得注意的是,作为标准库的代码,相关注释和代码都非常完美,真的可以看的神清气爽。

8.2 Redis 客户端

这个Golang实现的Redis客户端,是怎么实现连接池的。这边的思路非常奇妙,还是能学习到不少好思路。当然了,由于代码注释比较少,啃起来第一下还是有点迷糊的

https://github.com/go-redis/redis/blob/master/internal/pool/pool.go 

**1. 连接池结构 **

type ConnPool struct {
    ...
    queue chan struct{}

    connsMu      sync.Mutex
    conns        []*Conn
    idleConns    []*Conn
    poolSize     int
    idleConnsLen int

    stats Stats

    _closed  uint32 // atomic
    closedCh chan struct{}
}

我们可以看到里面存储连接的结构还是slice。但是我们可以重点看看queueconnsidleConns这几个变量,后面会提及到。但是值得注意的是!我们可以看到,这里有两个[]*Conn结构:connsidleConns,那么问题来了: 到底连接存在哪里?

2. 新建连接池连接

我们先从新建连接池连接开始看:

func NewConnPool(opt *Options) *ConnPool {
    ....
    p.checkMinIdleConns()

    if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
        go p.reaper(opt.IdleCheckFrequency)
    }
    ....
}

初始化连接池的函数有个和前面两个不同的地方。

  1. checkMinIdleConns方法,在连接池初始化的时候就会往连接池填满空闲的连接。
  2. go p.reaper(opt.IdleCheckFrequency)则会在初始化连接池的时候就会起一个go程,周期性的淘汰连接池里面要被淘汰的连接。

3. 获取连接

func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
    if p.closed() {
        return nil, ErrClosed
    }

    //这边和前面sql获取连接函数的流程先不同。sql是先看看连接池有没有空闲连接,有的话先获取不到再排队。这边是直接先排队获取令牌,排队函数后面会分析。
    err := p.waitTurn(ctx)
    if err != nil {
        return nil, err
    }
    //前面没出error的话,就已经排队轮候到了。接下来就是获取的流程。
    for {
        p.connsMu.Lock()
        //从空闲连接里面先获取一个空闲连接。
        cn := p.popIdle()
        p.connsMu.Unlock()

        if cn == nil {
            // 没有空闲连接时候直接跳出循环。
            break
        }
        // 判断是否已经过时,是的话close掉了然后继续取出。
        if p.isStaleConn(cn) {
            _ = p.CloseConn(cn)
            continue
        }

        atomic.AddUint32(&p.stats.Hits, 1)
        return cn, nil
    }

    atomic.AddUint32(&p.stats.Misses, 1)

    // 如果没有空闲连接的话,这边就直接新建连接了。
    newcn, err := p.newConn(ctx, true)
    if err != nil {
        // 归还令牌。
        p.freeTurn()
        return nil, err
    }

    return newcn, nil
}

我们可以试着回答开头那个问题:连接到底存在哪里?答案是从cn := p.popIdle()这句话可以看出,获取连接这个动作,是从idleConns里面获取的,而里面的函数也证明了这一点。同时我的理解是:

  1. sql的排队意味着我对连接池申请连接后,把自己的编号告诉连接池。连接那边一看到有空闲了,就叫我的号。我答应了一声,然后连接池就直接给个连接给我。我如果不归还,连接池就一直不叫下一个号。
  2. redis这边的意思是,我去和连接池申请的不是连接而是令牌。我就一直排队等着,连接池给我令牌了,我才去仓库里面找空闲连接或者自己新建一个连接。用完了连接除了归还连接外,还得归还令牌。当然了,如果我自己新建连接出错了,我哪怕拿不到连接回家,我也得把令牌给回连接池,不然连接池的令牌数少了,最大连接数也会变小。

而:

func (p *ConnPool) freeTurn() {
    <-p.queue
}
func (p *ConnPool) waitTurn(ctx context.Context) error {
...
    case p.queue <- struct{}{}:
        return nil
...
}

就是在靠queue这个chan来维持令牌数量。

那么conns的作用是什么呢?我们可以来看看新建连接这个函数:

4. 新建连接

func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
    cn, err := p.dialConn(ctx, pooled)
    if err != nil {
        return nil, err
    }

    p.connsMu.Lock()
    p.conns = append(p.conns, cn)
    if pooled {
        // 如果连接池满了,会在后面移除。
        if p.poolSize >= p.opt.PoolSize {
            cn.pooled = false
        } else {
            p.poolSize++
        }
    }
    p.connsMu.Unlock()
    return cn, nil
}

基本逻辑出来了。就是如果新建连接的话,我并不会直接放在idleConns里面,而是先放conns里面。同时先看池子满了没有。满的话后面归还的时候会标记,后面会删除。那么这个后面会删除,指的是什么时候呢?那就是下面说的归还连接的时候了。

5. 归还连接

func (p *ConnPool) Put(cn *Conn) {
    if cn.rd.Buffered() > 0 {
        internal.Logger.Printf("Conn has unread data")
        p.Remove(cn, BadConnError{})
        return
    }
    //这就是我们刚刚说的后面了,前面标记过不要入池的,这边就删除了。当然了,里面也会进行freeTurn操作。
    if !cn.pooled {
        p.Remove(cn, nil)
        return
    }

    p.connsMu.Lock()
    p.idleConns = append(p.idleConns, cn)
    p.idleConnsLen++
    p.connsMu.Unlock()
    //我们可以看到很明显的这个归还号码牌的动作。
    p.freeTurn()
}

其实归还的过程,就是从conns转移到idleConns的过程。当然了,如果新建这个连接时候发现已经超卖了,后面归还时候就不转移,直接删除了。

等等,上面的逻辑似乎有点不对?我们来理一下获取连接流程:

  1. waitTurn,拿到令牌。而令牌数量是根据pool里面的queue决定的。
  2. 拿到令牌了,去库房idleConns里面拿空闲的连接。没有的话就自己newConn一个,并且把他记录到conns里面。
  3. 用完了,就调用put归还:也就是从conns转移到idleConns。归还的时候就检查在newConn时候是不是已经做了超卖标记了。是的话就不转移到idleConns

我当时疑惑了好久,既然始终都需要获得令牌才能得到连接,令牌数量是定的。为什么还会超卖呢?翻了一下源码,我的答案是:

虽然Get方法获取连接是newConn这个私用方法,受到令牌管制导致不会出现超卖。但是这个方法接受传参:pooled bool。所以我猜是担心其他人调用这个方法时候,不管三七二十一就传了true,导致poolSize越来越大。

总的来说,redis这个连接池的连接数控制,还是在queue这个我称为令牌的chan进行操作。

6. 总结

上面可以看到,连接池的最基本的保证,就是获取连接时候的线程安全。但是在实现诸多额外特性时候却又从不同角度来实现。还是非常有意思的。但是不管存储结构是用chan还是还是slice,都可以很好的实现这一点。如果像sql或者redis那样用slice来存储连接,就得维护一个结构来表示排队等候的效果。

9. 简单练习

9.1 用户更改密码

前端Json数据

{"id":8,"name":"xiaohong7","password":"xiaobai","newpassword":"1234567"}

后端逻辑处理

// 定义一个结构体,接收前端传输的更多的参数
type ChangeUser struct {
	NewPassword string `gorm:"column:newpassword" json:"newpassword" binding:"required"`
	models.User
}

func ChangePwdHandler(c *gin.Context) {
    var user models.User
    // 接收前端传输的Json数据,并一一对应
    var changeUser ChangeUser
    
    if err := c.ShouldBindJSON(&changeUser); err != nil {
        log.Println("序列化错误!!!!",err)
        utils.Json(c,consts.BADREQUESTCODE,consts.BADREQUESTERROR,&defaultUser)
        return
    }
    
    // 查询需要修改密码的用户是否在用户表中,
    finiedUser := models.Db.Find(&user,"id = ?",changeUser.Id)
    if finiedUser.RowsAffected != 0{
        // 查询到了, 更新旧密码为新密码
        updatedUser := finiedUser.Select("PassWord").Update(&models.User{PassWord: changeUser.NewPassword})
        utils.Json(c,consts.OKCODE,consts.OK,updatedUser)
    }else{
        // 没查询到

	utils.Json(c,consts.NOTFINEDCODE,consts.NOTFINEDERROR,&defaultUser)
    }

  
}

9.2 时间类型的格式化

type MyTime struct {
	time.Time
}
func (m *MyTime) MarshalJSON() ([]byte,error) {
	formatted := fmt.Sprintf(`"%s"`,m.Format("2006-01-02 15:04:05"))
	fmt.Println("正在解析>>>>",formatted)
	return []byte(formatted),nil
}
func (t MyTime) Value() (driver.Value, error) {
	var zeroTime time.Time
	if t.Time.UnixNano() == zeroTime.UnixNano() {
		return nil, nil
	}
	return t.Time, nil
}

func (t *MyTime) Scan(v interface{}) error {
	value, ok := v.(time.Time)
	if ok {
		*t = MyTime{Time: value}
		return nil
	}
	return fmt.Errorf("can not convert %v to timestamp", v)
}

// 代替gorm.Model
type BaseModel struct {
	CreatedAt MyTime `gorm:"column:created_at" json:"created_at"`
	UpdatedAt MyTime `gorm:"column:updated_at" json:"updated_at"`
	DeletedAt MyTime `gorm:"autoUpdateTime:nano"`
}

9.3 根据不同的结构体返回不同的数据

// 定义需要返回的数据的结构

type FinedUser struct {
    ID       int    `gorm:"column:id;parimary_key" json:"id"`
    Name     string `gorm:"column:name;unique;omitempty" json:"name;omitempty" binding:"required"`
    Accounts string `gorm:"column:accounts;omitempty" json:"accounts;omitempty"`
    Level string `gorm:"column:level;omitempty" json:"level;omitempty"`
    BaseUserModel
}

// 查询语句,通过Scan指定,会根据FinedUser 结构体,返回对应结构体的数据
objDb = Db.Table("user").Where("name = ? AND password = ?", user.Name,user.PassWord).Scan(&finiedUser2)

10. 面试题

10.1 SQL污染问题

1. Gorm中的DB单例为什么可以并发访问数据库,直接使用DB操作SQL会有线程安全问题吗?Gorm中直接使用DB为什么没有线程安全问题,为什么Gorm单例也可以并发,为什么又会有SQL污染?

1. 很多人看了现代Go的理论这一篇文章,会觉得不应该使用全局的DB变量,觉得会有线程安全的问题,但是这篇文章将的是工程化的问题,可读性问题,没有线程安全问题

2. 一般来说全局变量都是会有线程安全的问题的,下面首先简单演示一下Gorm的链式调用

package main

import "fmt"

type Chain struct {
	ID   int
	Name string
}

func (c *Chain) Set(id int) *Chain {
	c.ID = id
	return c
}

func main() {
	// 1. 链式调用
	chain := &Chain{}
	chain.Set(1)
	// 2. 由于返回的是自身对象,内存地址是一致的,所以表明是同一个对象,就可以不用调用自身Set()方法
	fmt.Printf("%p\n", chain)
	fmt.Printf("%p\n", chain.Set(2))
}

// 打印结果
0xc0000040b0
0xc0000040b0

接下来再创建一个方法,返回一个结构体新的对象

package main

import "fmt"

type Chain struct {
	ID   int
	Name string
}

func (c *Chain) Set(id int) *Chain {
	c.ID = id
	return c
}

// 返回新的 Chain 对象
func (c *Chain) Get() *Chain {
	return &Chain{}
}

func main() {
	// 1. 链式调用
	chain := &Chain{}
	chain.Set(1)
	// 2. 由于返回的是自身对象,内存地址是一致的,所以表明是同一个对象
	fmt.Printf("%p\n", chain)
	fmt.Printf("%p\n", chain.Set(2))

	chain2 := &Chain{}
	chain2.Get()
	// 2. 由于返回的是新创建的对象,所以内存地址不一致,是不同的对象
	fmt.Printf("%p\n", chain2)
	fmt.Printf("%p\n", chain2.Get())
}


// 打印结果
0xc000004078
0xc000004078
0xc000004090
0xc0000040a8

3. Gorm 也是链式调用的

4. 理论部分

// 1. 为什么要有连接池
使用client和mysql,redis等建立连接都是非常耗时的,当我们执行完一个命令后关闭连接,然后下一个命令来了再创建连接,这是非常浪费资源的,所以一开始就建立好连接,并且保存起来,执行下一个命令的时候,直接从池里拿出来这个链接即可,这样会节省很多不必要的消耗

// 2. 初始连接数
连接池一旦初始化就创建一定数量的链接

// 3. 最少连接数,最大空闲时间
最少连接数喝最大空闲时间挂钩,如果连接一开始就初始化了10个,然后立马有了4个链接被使用,此时有6个是空闲的,这6个超过了最大的空闲时间,就会销毁(或者说取消)这6个链接,但是由于我设置了最少连接数为5,所以只会销毁5个,还会剩下1个空闲连接用于等待

5. Gorm的连接池调用

// 1. 由于 Golang 的连接池是内置在标准库的,所以执行gorm.Open() 时,返回的是连接池对象
DB,_ := gorm.Open(mysql.Open(url),config)

    // 2. 随便执行一个Find 命令,一路进入,到达Execute 方法,where语句只是在拼接语句,真正从连接池获取对象的方法是Find()或Exec()等需要跟数据库交互的方法,就会调用 Execute 方法,这才是真正的从连接池获取对象
for _,f := range p.fns{
    f(db)
}

// 3. 那么很明显,直接使用单例的DB对象去操作即可,每当我们使用命令的时候都会去连接池拿到connect对象,并且从连接池里拿到connect对象的这个操作是线程安全的,所以DB是支持并发的,

// 4. 我们可以直接拿到DB去操作,而不是放到这个struct 里,如果你用的是单例,name你直接open()一个新的对象,会导致你每个struct都变得庞大务必,这样无疑是浪费资源的

6. SQL 污染问题

6.1 getInstance()

// gorm的每一个链式操作都会执行一个getInstance方法,这个方法会做一些处理
// 1. 当Open() 的时候 会将DB的clone字段的值设置为1,当我们执行命令的时候,会执行getInstance 此时会判断clone字段
// 如果直接使用DB.Where("id",1)
// 当第一次执行getInstance时,此时clone为1,会返回一个新的连接池对象,此时没有传clone字段,即为它的默认值0,当执行第二个方法时,此时clone是0,就直接返回之前的保存着上一条语句执行结果的对象,此时两个条件都会被拼接在一起
type DB struct {
	*Config
	Error        error
	RowsAffected int64
	Statement    *Statement
	clone        int
}

func (db *DB) getInstance() *DB {
	if db.clone > 0 {
		tx := &DB{Config: db.Config, Error: db.Error}

		if db.clone == 1 {
			// clone with new statement
			tx.Statement = &Statement{
				DB:        tx,
				ConnPool:  db.Statement.ConnPool,
				Context:   db.Statement.Context,
				Clauses:   map[string]clause.Clause{},
				Vars:      make([]interface{}, 0, 8),
				SkipHooks: db.Statement.SkipHooks,
			}
		} else {
			// with clone statement
			tx.Statement = db.Statement.clone()
			tx.Statement.DB = tx
		}

		return tx
	}

	return db
}

6.2 代码演示

// 当第一次执行Where时会执行getInstance()方法,此时clone为1,会返回一个新的连接池对象,此时没有传clone字段,即为它的默认值0,当执行第二个方法时,此时clone是0,就直接返回之前的保存着上一条语句执行结果的对象,此时两个条件都会被拼接在一起

DB.Where("id = ?",1)
DB.Where("id = ?",2).Find(&User{})  // 此时的SQL语句为 select * from user where id = 1 and id=2
DB.Where("id = ?",3).Find(&User{})  // 此时的SQL语句为 select * from user where id = 1 and id=2 and id=3

6.3 解决方案

tx := DB.Where("id = ?",1).Session(&gorm.Session{})  // 使用session来设置clone字段为2
tx.Where("id = ?",2).Find(&User{})  // 此时的SQL语句为 select * from user where id = 1 and id=2
tx.Where("id = ?",3)   // clone字段为2,会返回之前id=1对象的克隆对象,就不会有id=2的条件保存了

7.并发情况下的SQL执行情况

// 并发为2
func use(){
    tx := DB.Where("id = ?",1)
    
    // 到这里 clone字段的值为0,还是和之前一样会有sql 污染问题,但是现在是同时执行2条sql,按道理来说,应该会出现线程安全问题,比如 select * from user where id = 1 前面一个协程构建了id为1的语句,后面一个协程也构建了id为1的雨具,此时的sql 语句可能会变成where id = 1 and id = 1,但是没有出现这种情况,因为并发是从外部进来的,外部进来的时候,都会执行DB.where,而那时候的clone 还是1呢,所以都会返回一个新的db对象,所以2个协程之间是互不影响的
    user := User{}
    
    // 此时的sql 语句为 where id = 1 and id =2 
    tx.Where("id = ?",2).Find(&user)
    
    // 又加了一条语句,那么这个sql还是会和上面所有的语句拼接,也就是还是会发生sql污染问题
    // 只不过是2条线程,各自拼接各自的,同样个不影响,所以他们之间是没有线程安全问题的,而sql污染问题,明显和并不并发没关系
    tx.Where("id = ?",3).Find(&user)
    
}

注意: 一定不要在操作sql的地方再手动开协程操作SQL,会发生线程不安全的问题

posted @ 2023-11-03 10:51  河图s  阅读(437)  评论(0)    收藏  举报