GORM入门(一)|青训营

137 阅读7分钟

GORM 入门

1.连接

需要下载mysql的驱动

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

简单连接

package main

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

const (
	host     = "127.0.0.1"
	port     = 3306
	user     = "root"
	password = "123456"
	dbname   = "gorm"
	timeout  = "10s"
)

func main() {
	// root:123456@tcp(127.0.0.1:3306)/gorm?
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=%s", user, password, host, port, dbname, timeout)
	//连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
	db, err := gorm.Open(mysql.Open(dsn))
	if err != nil {
		panic("连接数据库失败, error=" + err.Error())
	}
	// 连接成功
	fmt.Println(db)
}

高级配置

跳过默认事务

为了保证ACID,GORM会在事务内进行操作(增删改)。但是如果没有这方面的需求,则可以跳过默认事务,这样性能大概可以提升60%。

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

命名策略

GORM的命名策略是:表名为蛇形复数,字段名为蛇形单数。

type Student struct {
  Name      string
  Age       int
  MyTeacher string
}

GORM会为我们创建如下的表结构:

CREATE TABLE `students` (`name` longtext,`age` bigint,`my_teacher` longtext)

我们也可以在生成db对象时传入自定义的配置,如:

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix:   "f_",  // 表名前缀
    SingularTable: false, // 单数表名
    NoLowerCase:   false, // 关闭小写转换
  },
})

打印日志

GORM默认只打印错误和慢SQL,但是我们也可以自己配置:

var mysqlLogger logger.Interface
// 要显示的日志等级
mysqlLogger = logger.Default.LogMode(logger.Info)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
  Logger: mysqlLogger,
})

如果想自定义日志的显示,可以使用如下代码:

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
  logger.Config{
    SlowThreshold:                time.Second,   // 慢 SQL 阈值
    LogLevel:                     logger.Silent, // Log level
    IgnoreRecordNotFoundError:    true,        // 忽略ErrRecordNotFound(记录未找到)错误
    Colorful:                     false,         // 禁用彩色打印
  },
)

// 全局模式
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
  Logger: newLogger,
})

部分展示日志

var model Student
tx := db.Session(&Session{Logger: newLogger})
tx.First(&model)
// SELECT * FROM `students` ORDER BY `students`.`name` LIMIT 1

Debug单个操作

db.Debug().First(&model)

2.模型定义

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

先定义一张表:

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

tip:小写的变量是不会生成字段的

自动生成表结构

// 可以放多个
db.AutoMigrate(&Student{})

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

例如将Name修改为Name1,进行迁移,会多出一个name1的字段

生成的表结构如下:

CREATE TABLE `f_students` (`id` bigint unsigned AUTO_INCREMENT,`name` longtext,`email` longtext,PRIMARY KEY (`id`))

生成表的字段默认数据类型都很大

修改大小

我们可以使用GORM的标签对字段大小进行修改

有两种实现方法:

Name  string  `gorm:"type:varchar(12)"`
Name  string  `gorm:"size:2"`

GORM字段标签

type 定义字段类型

size 定义字段大小

column 自定义列名

primaryKey 将列定义为主键

unique 将列定义为唯一键

default 定义列的默认值

not null 不可为空

embedded 嵌套字段

embeddedPrefix 嵌套字段前缀

comment 注释

多个标签之前用 ; 连接

type StudentInfo struct {
  Email  *string `gorm:"size:32"` // 使用指针是为了存空值
  Addr   string  `gorm:"column:y_addr;size:16"`
  Gender bool    `gorm:"default:true"`
}
type Student struct {
  Name string      `gorm:"type:varchar(12);not null;comment:用户名"`
  UUID string      `gorm:"primaryKey;unique;comment:主键"`
  Info StudentInfo `gorm:"embedded;embeddedPrefix:s_"`
}

// 建表语句
CREATE TABLE `students` (
    `name` varchar(12) NOT NULL COMMENT '用户名',
    `uuid` varchar(191) UNIQUE COMMENT '主键',
    `s_email` varchar(32),
    `s_y_addr` varchar(16),
    `s_gender` boolean DEFAULT true,
    PRIMARY KEY (`uuid`)
)

3.单表操作

先使用gorm对单张表进行增删改查

表结构

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

添加记录

email := "xxx@qq.com"
// 创建记录
student := Student{
  Name:   "jinzhu",
  Age:    21,
  Gender: true,
  Email:  &email,
}
db.Create(&student)

有两个地方需要特别注意:

  1. 选用指针类型是为了更好地存储null指,同时在添加的时候也不要忘记传入指针
  2. Creat方法传入的是指针对象

由于我们在Create时传递的是指针对象,所以在初始化student时未对ID属性赋值,但是执行完Create后,对象的ID也被附上相应的值。

db.Create(&student)
fmt.Printf("%#v\n", student)
//main.Student{ID:0x1, Name:"jinzhu", Age:21, Gender:true, Email:(*string)(0xc0001eadd0)}

批量插入

Create方法支持插入多条数据。

var studentList []Student
for i := 0; i < 100; i++ {
  studentList = append(studentList, Student{
    Name:   fmt.Sprintf("机器人%d号", i+1),
    Age:    21,
    Gender: true,
    Email:  &email,
  })
}
db.Create(&studentList)

查询单条记录

一般我们使用Take方法进行单条记录的查询

var student Student
DB.Take(&student)
fmt.Println(student)

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。我们可以通过打印日志的方式来查看被执行的sql语句有什么不同:

db = db.Session(&gorm.Session{Logger: Log})
var student Student
db.Take(&student)  
// SELECT * FROM `students` LIMIT 1
db.First(&student) 
// SELECT * FROM `students` ORDER BY `students`.`id` LIMIT 1
db.Last(&student)  
// SELECT * FROM `students` ORDER BY `students`.`id` DESC LIMIT 1

根据主键查询

var student Student
db.Take(&student, 2)
fmt.Println(student)

student = Student{} // 重新赋值
db.Take(&student, "4")
fmt.Println(student)

Take方法的第二的参数,默认会根据主键查询,可以传入数字,也可以传入字符串。

根据其他条件查询

var student Student
db.Take(&student, "name = ?", "机器人66号")
fmt.Println(student)

使用?作为占位符,将查询条件“注入”其中。

编译出来的sql语句为:

SELECT * FROM `students` WHERE name = '机器人66号' LIMIT 1

通过占位符的方式可以做到有效预防sql注入,其原理是将参数全部转义:

db.Take(&student, "name = ?", "机器人66号' or 1=1;#")

//SELECT * FROM `students` WHERE name = '机器人66号\' or 1=1;#' LIMIT 1

根据struct查询

var student Student
// 只能有一个主要值
student.ID = 2
//student.Name = "jinzhu"
db.Take(&student)
fmt.Println(student)

获取查询结果

获取查询的记录数

count := db.Find(&studentList).RowsAffected

是否查询失败

err := db.Find(&studentList).Error

查询失败有查询为空,查询条件错误,sql语法错误

可以使用判断

var student Student
err := db.Take(&student, "xx").Error
switch err {
case gorm.ErrRecordNotFound:
  fmt.Println("没有找到")
default:
  fmt.Println("sql错误")
}

查询多条记录

var studentList []Student
db.Find(&studentList)
for _, student := range studentList {
  fmt.Println(student)
}

// 由于email是指针类型,所以看不到实际的内容
// 但是序列化之后,会转换为我们实际存储的内容
var studentList []Student
db.Find(&studentList)
for _, student := range studentList {

  data, _ := json.Marshal(student)
  fmt.Println(string(data))
}

根据主键列表查询

var studentList []Student
db.Find(&studentList, []int{1, 3, 5, 7})
db.Find(&studentList, 1, 3, 5, 7)  // 一样的
fmt.Println(studentList)

根据其他条件查询

db.Find(&studentList, "name in ?", []string{"jinzhu", "zhangsan"})

更新

更新的前提是可以查询到数据。

Save保存全部字段

用于单个记录的全字段更新,即使是零值(如0,false,null)也会保存

var student Student
db.Take(&student)
student.Age = 23
// 全字段更新
db.Save(&student)
// UPDATE `students` SET `name`='jinzhu',`age`=23,`gender`=true,`email`='xxx@qq.com' WHERE `id` = 1

更新指定字段

可以使用Select方法进行配合选择需要更新的字段

var student Student
db.Take(&student)
student.Age = 21
// 全字段更新
db.Select("age").Save(&student)
// UPDATE `students` SET `age`=21 WHERE `id` = 1

批量更新

例如我想给年龄21的学生,都更新一下邮箱

var studentList []Student
db.Find(&studentList, "age = ?", 21).Update("email", "is21@qq.com")

还有一种更简单的方式

db.Model(&Student{}).Where("age = ?", 21).Update("email", "is21@qq.com")
// UPDATE `students` SET `email`='is22@qq.com' WHERE age = 21

这样的更新方式也是可以更新零值的

更新多列

如果是结构体,它默认不会更新零值

email := "xxx@qq.com"
db.Model(&Student{}).Where("age = ?", 21).Updates(Student{
  Email:  &email,
  Gender: false,  // 不会更新
})

// UPDATE `students` SET `email`='xxx@qq.com' WHERE age = 21

如果想让他更新零值,用Select就好

email := "xxx1@qq.com"
DB.Model(&Student{}).Where("age = ?", 21).Select("gender", "email").Updates(Student{
  Email:  &email,
  Gender: false,
})
// UPDATE `students` SET `gender`=false,`email`='xxx1@qq.com' WHERE age = 21

如果不想多写几行代码,则推荐使用map

DB.Model(&Student{}).Where("age = ?", 21).Updates(map[string]any{
  "email":  &email,
  "gender": false,
})

删除

根据结构体删除

// student 的 ID 是 `10`
db.Delete(&student)
// DELETE from students where id = 10;

删除多个

db.Delete(&Student{}, []int{1,2,3})

// 查询到的切片列表
db.Delete(&studentList)

4.创建Hook

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

如果您已经为模型定义了指定的方法,它会在创建、更新、查询、删除时自动被调用。

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

此处展示一个在创建时默认邮箱有人名:

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

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

5.高级查询

重新构造一些数据用于查询

func main(){
  var studentList []Student
  DB.Find(&studentList).Delete(&studentList)
  studentList = []Student{
    {ID: 1, Name: "张三", Age: 32, Email: PtrString("zhangsan@qq.com"), Gender: true},
    {ID: 2, Name: "李四", Age: 18, Email: PtrString("lisi@163.com"), Gender: true},
    {ID: 3, Name: "王五", Age: 23, Email: PtrString("wangwu@qq.com"), Gender: true},
    {ID: 4, Name: "赵六", Age: 54, Email: PtrString("zhaoliu@qq.com"), Gender: true},
    {ID: 5, Name: "江七", Age: 23, Email: PtrString("jiangqi@163.com"), Gender: true},
    {ID: 6, Name: "jinzhu", Age: 14, Email: PtrString("jinzhu@163.com"), Gender: false},
    {ID: 7, Name: "yinzhu", Age: 25, Email: PtrString("yinzhu@126.com"), Gender: false},
    {ID: 8, Name: "tongzhu", Age: 26, Email: PtrString("tongzhu@126.com"), Gender: false},
    {ID: 9, Name: "tiezhu", Age: 21, Email: PtrString("tiezhu@126.com"), Gender: true},
  }
  db.Create(&studentList)
}

func PtrString(email string) *string {
  return &email
}

Where

等价于sql语句中的where

var users []Student
// 查询用户名是张三的
db.Where("name = ?", "张三").Find(&users)
fmt.Println(users)
// 查询用户名不是张三的
db.Where("name <> ?", "张三").Find(&users)
fmt.Println(users)
// 查询用户名包含 李四,王五
db.Where("name in ?", []string{"李四", "王五"}).Find(&users)
fmt.Println(users)
// 查询含zhu
db.Where("name like ?", "%zhu").Find(&users)
fmt.Println(users)
// 查询年龄大于23,是qq邮箱的
db.Where("age > ? and email like ?", "23", "%@qq.com").Find(&users)
fmt.Println(users)
// 查询是126邮箱的,或者是女的
db.Where("gender = ? or email like ?", false, "%@126.com").Find(&users)
fmt.Println(users)

使用结构体查询

使用结构体查询,会过滤零值

并且结构体中的条件都是and关系

// 会过滤零值
db.Where(&Student{Name: "李四", Age: 0}).Find(&users)
fmt.Println(users)

使用map查询

不会过滤零值

db.Where(map[string]any{"name": "李四", "age": 0}).Find(&users)
// SELECT * FROM `students` WHERE `age` = 0 AND `name` = '李四'
fmt.Println(users)

Not条件

和where中的not等价

// 排除年龄大于23的
db.Not("age > 23").Find(&users)
fmt.Println(users)

Or条件

和where中的or等价

db.Or("gender = ?", false).Or(" email like ?", "%@qq.com").Find(&users)
fmt.Println(users)

Select选择字段

db.Select("name", "age").Find(&users)
fmt.Println(users)
// 没有被选中的字段会被赋零值

可以使用Scan将选择的字段存入另一结构体中:

type User struct {
    Name string
    Age int
}
var users []User
db.Model(&Student{}).Select("name","age").Scan(&users)
//or db.Table("students").Select("name","age").Scan(&users)
fmt.Println(users)

Scan是通过column列名来扫描的,所以可以 通过gorm的字段标签修改列名

排序(Order)

//根据age排序
var users []Student
db.Order("age desc").Find(&users)
fmt.Println(users)
// desc    降序
// asc     升序

分页查询(Limit)

var students []Student
// 一页两条,第1页
db.Limit(2).Offset(0).Find(&users)
fmt.Println(students)
// 第2页
db.Limit(2).Offset(2).Find(&users)
fmt.Println(students)
// 第3页
db.Limit(2).Offset(4).Find(&users)
fmt.Println(students)

也可以定义变量来自动计算开始的页数

var students []Student
//一页几条
limit := 2
//第几页
page := 1
offset := (page - 1) * limit
db.Limit(limit).Offset(offset).Find(&users)
fmt.Println(students)

去重(Distinct)

//填入需要去重的字段
var ageList []int
db.Table("students").Select("age").Distinct("age").Scan(&ageList)
//or db.Table("students").Select("distinct age").Scan(&ageList)
fmt.Println(ageList)

分组查询

var ageList []int
// 查询男生的个数和女生的个数
db.Table("students").Select("count(id)").Group("gender").Scan(&ageList)
fmt.Println(ageList)
//[6,3]

我们可以得到分组结果,但是我们对于数字对应的性别无从知晓,所以我们可以对以上方法做出一下修改

type AgeGroup struct {
	Gender int
	Count  int `gorm:"column:count(id)"`
}
var age []AgeGroup
	// 查询男生的个数和女生的个数
	db.Table("students").Select("count(id)", "gender").Group("gender").Scan(&age)
	fmt.Println(age)
//[{1 6} {0 3}]

我们可以用Scan将我们Select出来的数据存储到另一个自定义的结构体中

原生Sql

gorm为我们提供Raw来执行原生Sql语句,但是查询出来的数据还是可以通过流式的方式进行数据加工

type AgeGroup struct {
  Gender int
  Count  int    `gorm:"column:count(id)"`
  Name   string `gorm:"column:group_concat(name)"`
}

var age []AgeGroup
db.Raw(`SELECT count(id), gender, group_concat(name) FROM students GROUP BY gender`).Scan(&age)

fmt.Println(age)
//[{0 3 jinzhu,yinzhu,tongzhu} {1 6 张三,李四,王五,赵六,江七,tiezhu}]

6.高级查询2

子查询

查询大于平均年龄的学生

//select * from students where age > (select avg(age) from students);
var students []Student
db.Model(Student{}).Where("age > (?)", DB.Model(Student{}).Select("avg(age)")).Find(&students)
fmt.Println(students)

参数命名

GORM 支持 sql.NamedArgmap[string]interface{}{} 形式的命名参数,例如:

var students []Student

db.Where("name = @name and age = @age", sql.Named("name", "张三"), sql.Named("age", 32)).Find(&students)
//or db.Where("name = @name and age = @age", map[string]any{"name": "张三", "age": 32}).Find(&students)
fmt.Println(students)

Find至map

GORM 允许扫描结果至 map[string]interface{}[]map[string]interface{},此时别忘了指定 ModelTable,例如:

res := map[string]interface{}{}
db.Model(&Student{}).First(&res, "id = ?", 1)


var res []map[string]any
db.Table("students").Find(&res)
fmt.Println(res)

查询引用(Scope)

Scopes 允许你指定常用的查询,可以在调用方法时引用这些查询:

func Age20(db *gorm.DB) *gorm.DB {
  return db.Where("age > ?", 20)
}

func main(){
  var students []Student
  db.Scopes(Age20).Find(&students)
  fmt.Println(students)
}

本篇为博文的上半部分,主要是对单表的增删改查,以及一些高级查询特性。本文的下半部分将围绕关系型数据库的核心----表与表之间存在关联的操作。