五、Gorm

发布时间 2023-07-17 11:56:28作者: xiaohaoge

5.1、gorm介绍

是当今比较热门的 golang 的 orm 操作数据库的工具,使用上主要是把 struct 类和数据库表进行映射,操作数据库时无需手写 sql。本质就是提供一组函数来帮助我们快速拼接 sql 语句,尽量减少 sql 的编写。

gorm文档gorm中文文档

安装包:

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

5.2、连接数据库

5.2.1、连接数据库

package main

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

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	dsn := "user:pwd@tcp(127.0.0.1:3306)/database?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	fmt.Println(db, err)
}

5.2.2、配置日志连接

package main

import (
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	dsn := "root:123456@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	// 创建日志对象
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		logger.Config{
			SlowThreshold: time.Second, // 慢SQL阈值
			LogLevel:      logger.Info, // log level
		},
	)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger, // 日志配置
	})
	if err != nil {
		panic("failed to connect database")
	}
	fmt.Println(db)
}

5.2.3、数据库连接池

// 全局数据库 db
var db *gorm.DB

// 包初始化函数,可以用来初始化 gorm
func init() {
  // 配置 dsn

  // err
  var err error
  // 连接 mysql 获取 db 实例
  db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
  if err != nil {
    panic("连接数据库失败, error=" + err.Error())
  }

  // 设置数据库连接池参数
  sqlDB, _ := db.DB()
  // 设置数据库连接池最大连接数
  sqlDB.SetMaxOpenConns(100)
  // 连接池最大允许的空闲连接数,如果没有sql任务需要执行的连接数大于20,超过的连接会被连接池关闭
  sqlDB.SetMaxIdleConns(20)
}

// 获取 gorm db,其他包调用此方法即可拿到 db
// 无需担心不同协程并发时使用这个 db 对象会公用一个连接,因为 db 在调用其方法时候会从数据库连接池获取新的连接
func GetDB() *gorm.DB {
  return db
}
func main() {
  // 获取 db
  db := tools.GetDB()
  // 执行数据库查询操作
}

5.3、数据库表操作

5.3.1、模型声明

我们以选课系统为例子:

学生表

老师表

班级表

课程表

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type BaseModel struct {
	ID         int       `gorm:"primaryKey"`
	CreateTime *time.Time `gorm:"autoCreateTime"`
	UpdateTime *time.Time `gorm:"autoCreateTime"`
	Name       string     `gorm:"type:varchar(32);unique;not null"`
}


type Teacher struct {
	BaseModel
	Tno    int
	Pwd    string `gorm:"type:varchar(100);not null"`
	Tel    string `gorm:"type:char(11);"`
	Birth  *time.Time
	Remark string `gorm:"type:varchar(255);"`
}


type Class struct {
	BaseModel
	Num     int
	TutorID int
	Tutor   Teacher
}

type Course struct {
	BaseModel
	Credit int
	Period int

	// 多对一
	TeacherID int
	Teacher   Teacher
}

type Student struct {
	BaseModel
	Sno    int
	Pwd    string `gorm:"type:varchar(100);not null"`
	Tel    string `gorm:"type:char(11);"`
	Gender byte   `gorm:"default:1"`
	Birth  *time.Time
	Remark string `gorm:"type:varchar(255);"`

	// 多对一
	ClassID int
	Class   Class `gorm:"foreignKey:ClassID"`
	// 多对多
	Courses []Course `gorm:"many2many:student2course;constraint:OnDelete:CASCADE;"`
}

注意事项:

  1. 默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间。

  2. 单独设置表名:

     func (u User) TableName() string {
       return "user"
     }
  3. GORM 定义一个 gorm.Model 结构体,其包括字段 IDCreatedAtUpdatedAtDeletedAt

     // gorm.Model 的定义
     type Model struct {
       ID        uint           `gorm:"primaryKey"`
       CreatedAt time.Time
       UpdatedAt time.Time
       DeletedAt gorm.DeletedAt `gorm:"index"`
     }

5.3.2、模型迁移

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

func main() {
	// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
	dsn := "root:123456@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local"
	// 创建日志对象
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags),
		logger.Config{
			SlowThreshold: time.Second, // 慢SQL阈值
			LogLevel:      logger.Info, // log level
		},
	)
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger, // 日志配置
	})
	if err != nil {
		panic("failed to connect database")
	}

	// 自动迁移
	db.AutoMigrate(&Teacher{})
	db.AutoMigrate(&Course{})
	db.AutoMigrate(&Class{})
	db.AutoMigrate(&Student{})
}

AutoMigrate支持建表,,如果表存在则不会再创建

-- 建表
db.AutoMigrate(&Teacher{})
-- 建表
db.Migrator().CreateTable(&Teacher{})
-- 建 3 表
db.AutoMigrate(&Teacher{}, &Class{}, &Course{})
-- 可以通过 Set 设置附加参数,下面设置表的存储引擎为 InnoDB
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&Teacher{})

检测表存在:

// 检测Teacher结构体对应的表是否存在
db.Migrator().HasTable(&Teacher{})
// 检测表名teachers是否存在
db.Migrator().HasTable("teachers")

5.3.3、表字段操作

AutoMigrate不支持字段修改删除,为了便秘数据意外丢失。

-- 删除 Teacher 结构体对应的表
db.Migrator().DropTable(&Teacher{})
-- 依据表名删除表
db.Migrator().DropTable("teachers")
-- 删除字段
db.Migrator().DropColumn(&Teacher{}, "Name")
-- 为字段添加索引
db.Migrator().CreateIndex(&Teacher{}, "Name")
-- 修改索引名
db.Migrator().RenameIndex(&Teacher{}, "Name", "Name2")
-- 为字段删除索引
db.Migrator().DropIndex(&Teacher{}, "Name")
-- 检查索引存在
db.Migrator().HasIndex(&Teacher{}, "Name")

5.4、数据库表记录操作

5.4.1、添加记录

(1)添加单表记录

t1 := Teacher{BaseModel: BaseModel{Name: "hao"}, Tno: 1001, Pwd: "123"}
db.Create(&t1)
fmt.Println(t1.ID)
t2 := Teacher{BaseModel: BaseModel{Name: "echo"}, Tno: 1002, Pwd: "234"}
db.Create(&t2)
t3 := Teacher{BaseModel: BaseModel{Name: "June"}, Tno: 1002, Pwd: "345"}
db.Create(&t3)

GORM 将生成一条 SQL 语句来插入数据并回填主键值

db.Debug() 会打印sql日志

(2)批量添加多对一关联表的记录

要有效地插入大量记录,请将切片传递给该Create方法。

class01 := Class{BaseModel: BaseModel{Name: "软件1班"}, TutorID: 1}
class02 := Class{BaseModel: BaseModel{Name: "软件2班"}, TutorID: 1}
class03 := Class{BaseModel: BaseModel{Name: "计算机科学与技术1班"}, TutorID: 2}
class04 := Class{BaseModel: BaseModel{Name: "计算机科学与技术2班"}, TutorID: 2}
classes := []Class{class01, class02, class03, class04}
db.Create(&classes)

for _, class := range classes {
fmt.Println(class.ID) // 1,2,3
}

course01 := Course{BaseModel: BaseModel{Name: "计算机网络"}, Credit: 3, Period: 16, TeacherID: 1}
course02 := Course{BaseModel: BaseModel{Name: "数据结构"}, Credit: 2, Period: 24, TeacherID: 1}
course03 := Course{BaseModel: BaseModel{Name: "数据库"}, Credit: 2, Period: 16, TeacherID: 2}
course04 := Course{BaseModel: BaseModel{Name: "数字电路"}, Credit: 3, Period: 12, TeacherID: 2}
course05 := Course{BaseModel: BaseModel{Name: "模拟电路"}, Credit: 1, Period: 8, TeacherID: 2}
courses := []Course{course01, course02, course03, course04, course05}
db.Create(&courses)

指定批量大小:db.CreateInBatches(users, 100 )

(3)添加多对多关联表的记录

// 绑定课程对象切片
var courses []Course
db.Where("name in ?", []string{"数据结构", "数据库"}).Find(&courses)
fmt.Println("courses:", courses)

// 添加学生1
s1 := Student{BaseModel: BaseModel{Name: "张三"}, Sno: 2001, Pwd: "123", ClassID: 1}
db.Create(&s1)

// 多对多添加方式1
s2 := Student{BaseModel: BaseModel{Name: "李四"},
              Sno:     2002,
              Pwd:     "234",
              ClassID: 1,
              Courses: courses,
             }
db.Create(&s2)

// 多对多添加方式2
s3 := Student{BaseModel: BaseModel{Name: "王五"}, Sno: 2003, Pwd: "234", ClassID: 1}
db.Create(&s3)
fmt.Println("s3 id:", s3.ID)
db.Model(&s3).Association("Courses").Append(courses) // 注意:Courses是多对多关联字段,不是关联表

// 先查询再操作
var student = Student{}
db.Where("name = ?", "王五").First(&student)
fmt.Println(student)
db.Model(&student).Association("Courses").Clear()
var courses []Course
db.Where("name in ?", []string{"数字电路", "模拟电路"}).Find(&courses)
db.Model(&student).Association("Courses").Append(courses)

5.4.2、单表查询

gorm 使用链式函数来查询的。

(1) 查询全部记录

// Find 查询多条记录,返回数组
// select * from students;
students := []Student{}  // 使用Find要声明数组,如果声明一个对象(var s Student),就会将第一个对象
result := db.Find(&students)
fmt.Println(result.RowsAffected)
fmt.Println(students)

for _, student := range students {
  fmt.Println(student.ID, student.Name, student.Sno)
}

/*   

    students := []Student{} 
     
    (1) select * from students;
    5,2022-11-29 16:53:23.823,2022-11-29 16:53:23.823,张三,2001,123,"",1,2022-11-15 00:00:00,"",1,1
    6,2022-11-29 16:53:50.756,2022-11-29 16:53:50.756,李四,2002,123,"",1,2022-11-10 00:00:00,"",3,2
    7,2022-11-29 16:54:11.471,2022-11-29 16:54:11.471,王五,2003,123,"",1,2022-11-18 00:00:00,"",2,3

    (2) 
    [
		Student{
			BaseModel:		{1 2022-11-03 17:59:02.188 +0800 CST 2022-11-03 17:59:02.188 +0800 CST 张三}
			Sno:				   2001
			Pwd:				   123
			Gender:				   1
			Birth:				 <nil>
			ClassID:				 1
			Class对象:			{
                        BaseModel: {0 <nil> <nil> }
                        Num: 0
                        TutorID: 0
                        Tutor对象: {{0 <nil> <nil> } 0 <nil>}
                        students: []
							  		 }
			Courses:				[]

		 },
		 ...
		 ]
*/

(2)查询单条记录

student := Student{}
// Take 查询一条记录
// SELECT * FROM `user` LIMIT 1
db.Take(&student)

// First 根据主键 id 排序后的第一条
// SELECT * FROM `user` ORDER BY `id` LIMIT 1
db.First(&student)

// Last 根据主键 id 排序后最后一条
// SELECT * FROM `user` ORDER BY `id` DESC LIMIT 1
db.Last(&student)

// Where 表示条件,其中写 sql 部分
// SELECT * FROM `user` WHERE (id = '10') LIMIT 1
db.Where("id = ?", 10).Take(&user)

(3) Where查询

基于string的where语句

var student Student
db.Where("name = ?", "李四").First(&student)
fmt.Println(student)

var students []Student
db.Where("Sno between ? and ?", 2001, 2003).Find(&students)
fmt.Println(students)

var students []Student
db.Where("Sno in ?", []int64{2001, 2003}).Find(&students)
fmt.Println(students)

var students []Student
db.Where("name like  ?", "李%").Find(&students)
fmt.Println(students)

var students []Student
db.Where("create_time >  ?", "2021-01-01 00:00:00").Find(&students)
fmt.Println(students)

var students []Student
db.Where("create_time > ? AND create_time < ?", "2022-01-01 00:00:00", "2022-12-31 00:00:00").Find(&students)
fmt.Println(students)

基于Struct & Map 条件的where语句

// Struct条件
var students []Student
db.Where(&Student{BaseModel: BaseModel{Name: "张三"}, Gender: 1}).Find(&students)
fmt.Println(students)

// 注意: 使用结构作为条件查询时,GORM 只会查询非零值字段。例如:
db.Where(&Student{BaseModel: BaseModel{Name: "张三"}, Gender: 0}).Find(&students)
fmt.Println(students)
// SELECT * FROM `students` WHERE `students`.`name` = '张三'

// Map条件
db.Where(map[string]interface{}{"Name": "张三", "Gender": 0}).Find(&students)
fmt.Println(students)

(4)其他查询语句

-- Select语句 表示选择,其中写 sql 部分
-- SELECT name,sno FROM `students` WHERE id = 10 LIMIT 1
var student Student
db.Select("name,sno").Where("id = ?", 10).Take(&student)
log.Println(student)
db.Omit("name", "sno").Find(&students)
fmt.Println(students)

-- Not语句  
var students []Student
db.Not("sno between ? and ?", 2001, 2002).Find(&students) --  Not语句:用法类似于Where
fmt.Println(students)

-- Or语句  
var students []Student
db.Where("sno = ?", 2001).Or("name like  ?", "王%").Find(&students)
fmt.Println(students)

-- Order 表示排序方式,其中写 sql 部分
-- SELECT * FROM `students` WHERE create_time >= '2018-11-06 00:00:00' ORDER BY create_time desc,id
var students []Student
db.Where("create_time >= ?", "2018-11-06 00:00:00").Order("create_time desc,id").Find(&students)
log.Println(students)

-- Limit Offset 分页常用
--SELECT * FROM `students` ORDER BY create_time desc LIMIT 10 OFFSET 1
var students []Student
db.Order("create_time desc").Limit(10).Offset(1).Find(&students)
log.Println(students)

-- Count 计算行数
-- SELECT count(*) FROM `students`
var total int64
db.Model(Student{}).Count(&total)
fmt.Println(total)

-- Group Having 分组查询,其中写 sql 部分,Group 必须和 Select 一起连用
type Result struct {
		ClassID int
		Total   int
	}
var results []Result
--  SELECT class_id, Count(*) as total FROM `students` GROUP BY `class_id` HAVING total>1
db.Model(Student{}).Select("class_id,Count(*) astotal").Group("class_id").Having("total>0").Scan(&results)
log.Println(results)

5.4.3、删除记录

// 删除一条记录
student := Student{BaseModel: BaseModel{ID: 3}}
db.Delete(&student)
// 按条件删除
db.Where("sno between ? and ?", 2001, 2002).Delete(Student{})
// 删除所有记录
db.Where("1 = 1").Delete(&Student{})

5.4.4、更新记录

// Save 更新某条记录的所有字段
stu01 := Student{}
db.First(&stu01)
stu01.Name = "张三三"
db.Save(&stu01)

// Update 基于主键更新某条记录的单个字段
stu02 := Student{BaseModel: BaseModel{ID: 1}}
db.Model(&stu02).Update("name", "张三")

// Update 跟新所有记录的单个字段
db.Model(&Student{}).Update("price", 25)

// Update 自定义条件而非主键记录更新某字段
db.Model(&Student{}).Where("create_time > ?", "2018-11-06 20:00:00").Update("price", 25)

// Update 更新多个字段
// 通过 `struct` 更新多个字段,不会更新零值字段
db.Model(&Student{}).Where("id = ?", 2).Updates(Student{Sno: 2002, Gender: 0})
// 通过 `map` 更新多个字段,零值字段也会更新
db.Model(&Student{}).Where("id = ?", 2).Updates(map[string]interface{}{"gender": 1, "sno": 2002})

// 更新表达式
Update("stock", gorm.Expr("stock + 1"))
db.Model(&Class{}).Update("Num", gorm.Expr("Num+1"))
db.Model(&Student{}).Update("Pwd", gorm.Expr("Sno"))

5.4.5、关联表查询

(1)Preload(子查询 )

GORM 允许在 Preload 的其它 SQL 中直接加载关系

案例1:查询李四的班级的名称

// 手动查询
s := Student{}
db.Where("name = ?", "李四").Find(&s)
fmt.Println(s)
class := Class{}
db.Where("id = ?", s.ClassID).Find(&class)
fmt.Println(class.Name)

// Preload 预加载
s := Student{}
db.Where("name = ?", "李四").Preload("Class").Find(&s)
// SELECT * FROM `students` WHERE name = '李四'
// SELECT * FROM `classes` WHERE `classes`.`id` = 1
fmt.Println(s)
fmt.Println(s.Sno)
fmt.Println(s.Class.Name)
fmt.Println(s.Courses)

案例2:查询张三的班级和所选课程

s := Student{}
db.Where("name = ?", "李四").Preload("Class").Preload("Courses").Find(&s)
// "gorm.io/gorm/clause"
// db.Where("name = ?", "lisi").Preload(clause.Associations).Find(&s)

// SELECT * FROM `students` WHERE name = '李四'
// SELECT * FROM `classes` WHERE `classes`.`id` = 2
// SELECT * FROM `student2course` WHERE `student2course`.`student_id` = 11
// SELECT * FROM `courses` WHERE `courses`.`id` IN (2,3)
fmt.Println(s)
fmt.Println(s.Sno)
fmt.Println(s.Class.Name)
fmt.Println(s.Courses)
fmt.Println(s.Courses[1])

案例3:查询软件一班所有学生【反向查询】

// class表添加成员变量:Students
type Class struct {
BaseModel
Num     int
TutorID int
Tutor   Teacher
// 一对多
Students []Student    // 反向查询字段
}
class := Class{}
db.Where("name", "软件1班").Preload("Students").Find(&class)
// SELECT * FROM `classes` WHERE `name` = '软件1班'
// SELECT * FROM `students` WHERE `students`.`class_id` = 1
fmt.Println(class)
fmt.Println(class.Students)
for _, stu := range class.Students {
    fmt.Println(stu.ID,stu.Name)
}

案例4:查询软件一班所有学生以及所学课程【嵌套预加载】

class := Class{}
db.Where("name", "软件1班").Preload("Students.Courses").Find(&class)
fmt.Println(class)
fmt.Println(class.Students)
for _, stu := range class.Students {
  fmt.Println(stu.ID, stu.Name, stu.Courses)
}

案例5:哪些学生报了数据结构课程

// Course表添加成员变量:Students

 type Course struct {
   BaseModel
   Credit int8
   Period int8
 ​
   // 多对一
   TeacherID int
   Teacher   Teacher
 ​
   // 多对多
  Students []Student `gorm:"many2many:student2course;"`
 }
course := Course{}
 db.Where("name", "数据结构").Preload("Students").Find(&course)
 // db.Where("name", "数据结构").Preload("Students.Courses").Find(&course)
 fmt.Println(course)
 fmt.Println(course.Students)
 for _, stu := range course.Students {
   fmt.Println(stu.ID, stu.Name, stu.Courses)
 }

(2)Joins 查询

Preload 在一个单独查询中加载关联数据。而 Join Preload 会使用 inner join 加载关联数据

案例1:查询李四的班级名称【一对多】

var stu = Student{}
db.Where("students.name = ?", "李四").Joins("Class").Find(&stu)
fmt.Println(stu)
fmt.Println(stu.Class.Name)

案例2:查询李四的所选课程【多对多】

stu := Student{BaseModel: BaseModel{ID: 2}}
 var courses []Course
 db.Model(&stu).Association("Courses").Find(&courses)
 fmt.Println(courses)
 /*
      SELECT `courses`.`id`,`courses`.`create_time`,`courses`.`update_time`,`courses`.`name`,`courses`.`credit`,`courses`.`period`,`courses`.`teacher_id`
      FROM `courses` JOIN `student2course`
      ON `student2course`.`course_id` = `courses`.`id`
      AND `student2course`.`student_id` = 2
 */
 ​
 //db.Model() 中必须是ID筛选