Go后端技术栈之GORM库 part two

Posted by OuterCyrex on July 6, 2024

二.基础语句

一.HOOK函数

通过定义HOOK函数,可以在每次进行特定操作前运行HOOK,实现特定的效果。

注意HOOK函数的命名要遵循Before/After + 对应的操作,如下述的BeforeCreateAfterUpdate

1
2
3
4
5
func (user *Student) BeforeCreate(db *gorm.DB) error {
	age := 21
	user.Age = age
	return nil
}

上述的HOOK函数在每次Create操作前,将对应数据的Age字段改为了21

1
2
3
DB.Create(&Student{Name: "Outer114号"})
//对应的SQL语句
INSERT INTO `students` (`name`,`age`) VALUES ('Outer114号',21);

HOOK定义时便确定了其对象,如:

1
2
3
4
5
func (user *Student) AfterUpdate(db *gorm.DB) error {
	age := user.Age
	fmt.Println("AfterUpdate used", age)
	return nil
}

则这是一个在Update操作后会实施的HOOK函数

1
2
3
DB.Find(&student, 2).Update("age", 10)
//返回值
AfterUpdate used 10

注:创建HOOK时一定要注意方法的对象为指针类型

二.基础查询语句

1.WHERE查询

GORM中存在WHERE函数可以便于查询对应的数据。

1
2
3
4
5
6
7
8
9
DB.Find(&slice, "name = ?", "Outer")
DB.Where("name = ?", "Outer").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE name = 'Outer';
/*--------------------------------------------------*/
DB.Find(&slice, "not name = ?", "Outer")
DB.Where("not name = ?", "Outer").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE not name = 'Outer';

在常见查询中,可以通过Find直接实现查询操作。实际上,Find函数的定义时直接使用了Where函数来处理传入的第二行参数conds

1
2
3
4
5
6
7
8
9
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
	/*............................................*/
	if len(conds) > 0 {
		if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
			tx.Statement.AddClause(clause.Where{Exprs: exprs}) //在此处使用了`Where`来查询
		}
	}
    /*............................................*/
}

相当于Find函数的传入的第二个参数可以直接填入Where对应的内容。但我们仍然可以在日常查询中使用Where来提高代码可读性,便于读取查询条件

为了便于读取查询条件,接下来的查询语句统一使用Where

Where可以实现很多SQL语言的高级查询操作,如模糊查询

1
2
3
4
5
6
7
DB.Where("name like ?", "Outer%").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE name like 'Outer%'
/*---------------------------------------------------------*/
DB.Where("ID between 10 AND 50").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE ID between 10 AND 50;

此外,可以通过多个Where限定条件,来实现SQLAND的操作

1
2
DB.Where("Name = ? AND Age = ?", "Outer", 10).Find(&slice)
DB.Where("Name = ?", "Outer").Where("Age = ?", 10).Find(&slice)

上述两行代码的效果是一致的。

除了通过原生SQL语句查询,还可以使用结构体查询映射查询(虽然十分不建议就是了)

1
2
3
4
DB.Where(&Student{Name: "Outer"}).Where(&Student{Age: 10}).Find(&slice)
DB.Where(map[string]any{"Name": "Outer"}).Where(map[string]any{"Age": 10}).Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE `students`.`name` = 'Outer' AND `students`.`age` = 10

注意:使用映射查询时,一定要严格遵守map[string]any的格式,不能随意将any改动为其他数据类型,否则可能会导致查询失败

2.NOT和OR查询

NotOr是与Where一样的查询函数,但二者均可在Where内实现。

1
2
3
4
DB.Where("not name like ?", "Outer__号").Find(&slice)
DB.Not("name like ?", "Outer__号").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE NOT name like 'Outer__号';

可以理解为Not即为Where Not的缩写,即在Where函数内自带一个NOT

1
2
3
4
DB.Where("name = ?", "Outer").Or("ID = ?", 3).Find(&slice)
DB.Where("name = ? OR ID = ?", "Outer", 3).Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE name = 'Outer' OR ID = 3;

推荐在查询时使用Where来直接实现这些操作,因为SQL语言的可靠性较高。

3.SELECT语句

通过SELECT语句,可以限定查询的内容

1
2
3
4
5
var slice []Student
DB.Select("Name").Find(&slice)
fmt.Println(slice)
//对应的SQL语句
SELECT `name` FROM `students`;

上述代码即在查询过程中,只返回查询到的Name值,而其他的字段返回默认的空值。

对于多列数据的查询,可以直接将对应字段的字符串全部输入,也可以选择传入一个[]string切片来实现

1
2
DB.Select("ID", "Name", "Age").Find(&slice)
DB.Select([]string{"ID", "Name", "Age"}).Find(&slice)

如果我们想去除为空的字段数据,将查询到的结果储存一个子表,则可以通过Scan函数

1
2
3
4
5
6
7
type Outer struct {
		ID   int    `gorm:"type:int"`
		Name string `gorm:"type:varchar(20)"`
	}
var outerslice []Outer
DB.Select("ID", "Name").Find(&slice).Scan(&outerslice)
fmt.Println(outerslice)

此时我们输出查询结果,便不会出现为空的字段

Scan函数的查询是严格按照列名(Column)来判断的,若列名不同,则将不会返回结果。

4.指定表名

我们在使用上述的Scan函数时,会发现Find函数进行了多余的查询操作,但直接删除Find则无法指定Scan的表为哪张表。

此时我们可以选择直接指定表名,有两种方法

1
2
DB.Table("students").Select("ID", "Name").Scan(&outerslice)
DB.Model(&Student{}).Select("ID", "Name").Scan(&outerslice)

不难看出,二者类似于MySQL原生语句中的USE Table

但注意Table传入的是表名,一定是一个字符串。而Model传入的是一个空结构体指针

三.基础关键字

1.排序

GORM中,实现排序需要我们使用Order语句,且排序分为降序DESC升序ASC

关键字 升降序
DESC 降序
ASC 升序

Order内只需填一个字符串即可,内容为"字段名 升降序"

1
2
3
4
5
DB.Order("ID ASC").Where("Name Like ?", "Outer%").Find(&slice)
DB.Order("Age DESC").Where("Name Like ?", "Outer%").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE Name Like 'Outer%' ORDER BY ID ASC;
SELECT * FROM `students` WHERE Name Like 'Outer%' ORDER BY Age DESC;

同样,我们可以在字符串内添加AND关键字来实现多段排序

1
2
3
DB.Order("Age AND ID ASC").Where("Name Like ?", "Outer%").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE Name Like 'Outer%' ORDER BY Age AND ID ASC;

2.分页

SQL中,我们通过LIMIT ... OFFSET ...来实现分页操作,在GORM中我们同样使用这两个关键字

1
2
3
DB.Limit(2).Offset(0).Where("Name Like ?", "Outer%").Find(&slice)
//对应的SQL语句
SELECT * FROM `students` WHERE Name Like 'Outer%' LIMIT 2 OFFSET 0;

注意,Offset传入的是第几条数据,而不是第几,因此每次迭代Offset都需要加上Limit的值来遍历所有数据。

1
2
3
4
5
6
limit, offset := 4, 0
for offset < 100 {
	DB.Limit(limit).Offset(offset).Where("Name Like ?", "Outer%").Find(&slice)
	offset += limit
	fmt.Println(slice)
}

3.去重

MySQL中,去重选用的是Distinct函数,这一点在GORM中同样适用

1
2
3
4
5
6
7
8
type Age struct {
		Age int
	}
var ageSlice []Age
DB.Distinct("Age").Find(&slice).Scan(&ageSlice)
fmt.Println(ageSlice)
//对应的SQL语句
SELECT DISTINCT `age` FROM `students`;

注意,GORMDistinct函数自带Select效果,因此不应写作Select("Name").Distinct("Name"),这样反而会造成语句冗余

此外,我们更常用的做法是直接在Select语句内加入Distinct关键字

1
2
3
DB.Distinct("Distinct Age").Find(&slice)
//对应的SQL语句
SELECT DISTINCT Distinct Age FROM `students`;

4.分组

通过Group函数,我们可以通过对对应的字段进行分组来查询数据

1
2
3
4
5
var count []int
DB.Model(&Student{}).Select("COUNT(*)").Group("Age").Scan(&count)
fmt.Println(count)
//对应的SQL语句
SELECT COUNT(*) FROM `students` GROUP BY `Age`;

其中,COUNT()SQL语句中的函数,由于GORM只是将MySQL抽象出来,其底层仍然可以实现SQL语句。

Group即按照Age的情况进行分组

此外,我们会发现,如果想查询Count对应的是哪些数据,会出现一定的错误

1
2
3
4
5
6
type Group struct {
		Count int
		Age   int
	}
var count []Group
DB.Model(&Student{}).Select("COUNT(*)", "Age").Group("Age").Scan(&count)

上述代码的输出结果中,COUNT(*)字段的结果全为0,这是由于COUNT(*)在查询结果的表对应的列名就是COUNT(*),无法与我们构建的结构体字段名对应。

我们没必要将结构体的对应字段改为COUNT(*)(实际上GO不允许字段名有括号),只需要在Select语句中加上AS关键字即可

1
DB.Model(&Student{}).Select("COUNT(*) AS count", "Age").Group("Age").Scan(&count)

此时便可以正常输出查询结果了。

此外,我们也可以加入特定的group_concat关键字来显示分组结果

1
DB.Model(&Student{}).Select("group_concat(Name) AS count", "Age").Group("Age").Scan(&count)

四.原生SQL语句

GORM为了保证原生SQL语句的使用,设置了Raw函数,我们可以在Raw函数内输入原生SQL语句字符串来进行操作

1
DB.Raw("SELECT COUNT(*) AS count,`age` FROM `students` GROUP BY `Age`").Scan(&count)

此时,我们便实现了原生SQL语句的输入和使用,但输出的接口仍然需要自行定义。