Go后端技术栈之GORM库 part one

Posted by OuterCyrex on July 5, 2024

一.数据处理

一.引入

1.配置开发环境

首先在创建项目时增添GOPROXY路径

1
GOPROXY=http://goproxy.cn,direct

之后获取GORM文件以及gin文件

1
2
3
go get gorm.io/driver/mysql
go get gorm.io/gorm
go get github.com/gin-gonic/gin

2.连接

连接MySQL数据库首先需要DSN字符串。

DSN(Data Source Name)是一串用于在数据库连接标识定位数据库的特殊字符串。这串字符串包含了数据库连接所需的关键信息,以便应用程序能够准确地连接到目标数据库

1
2
3
4
5
6
username := "root"
password := "123456"
host := "127.0.0.1"
port := 3306
Dbname := "gorm"
timeout := "10s"

上述数据是我们连接数据库必须的数据。

1
2
3
4
5
6
7
8
9
10
11
dsn := 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(dsn))
if err != nil{
    panic("ERROR")
}

一般使用的dsn即上述代码中Sprintf的内容。

此时便可以定义一个全局变量来存储db的信息

1
var DB *gorm.DB

为保证数据的一致性,GORM会在事务里执行写入操作(创建、更新、删除),若无此需求则可进行关闭,来获得性能提升

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

3.日志

通过设置日志,可以查看GORMSQL语句和错误信息

1
mysqllogger := logger.Default.LogMode(logger.Info)

其中mysqllogger的数据类型为logger.Interface

之后只需将该日志接入数据库中即可

1
2
3
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger:mysqllogger,
	})

我们也可以不在初始化的时候引入日志,而是创建会话日志

1
2
3
DB = DB.Session(&gorm.Session{
		Logger: mysqllogger,
	})

或者我们可以直接使用GORM提供的Debug()方法来实现日志,分割线下方是Debug()方法的源码,可以发现Debug也是使用会话日志来实现的。

1
2
3
4
5
6
7
8
_ = DB.Debug().AutoMigrate(&Student{})
/*----------------------------------------------*/
func (db *DB) Debug() (tx *DB) {
	tx = db.getInstance()
	return tx.Session(&Session{
		Logger: db.Logger.LogMode(logger.Info),
	})
}

二.创建表格

1.创建

GORM内,一个结构体(类)对应一个,因此,我们可以通过定义结构体来定义一个

1
2
3
4
5
6
type Student struct {
	ID   int
	Name string
	Age  int
}
_ = DB.AutoMigrate(&Student{})

此处AutoMigrate便会根据传入的空结构体的字段信息来创建的名称便是该结构体的名称。

创建表的过程存在一定的自定义内容,其位于schema.NamingStrategy

1
2
3
4
5
6
7
type NamingStrategy struct {
    TablePrefix         string
    SingularTable       bool
    NameReplacer        Replacer
    NoLowerCase         bool
    IdentifierMaxLength int
}

其中,TablePrefix可以让创建的表增添某一特定前缀SingularTable即命名表时更倾向于使用单数表名NoLowerCase则禁止将大写转为小写

此外,我们可以通过定义指针类型来实现数据库中的NULL,如下述代码中Email字段为*string类型,此时可以向该字段传空值。

1
2
3
4
5
6
type Student struct {
	ID   int
	Name string
	Email *string
}
_ = DB.AutoMigrate(&Student{})

AutoMigrate的逻辑是只新增,不删除、不修改。

2.自定义类型

但我们会发现,通过默认设置的AutoMigrate创建的表存在一定问题

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

数据类型使用的是bigintlongtext,占用的存储空间过大。

此时,我们可以通过在结构体中增添gorm标签来限制大小

1
2
3
4
5
6
7
8
9
10
11
type Student struct {
	ID   int  `gorm:"size:10"`
	Name string `gorm:"size:16"`
	Age  int  `gorm:"size:3"`
}
//或者直接定义数据类型
type Student struct {
	ID   int    `gorm:"type:int"`
	Name string `gorm:"type:varchar(20)"`
	Age  int    `gorm:"type:tinyint(3)"`
}

此时再运行

1
2
ALTER TABLE `students` MODIFY COLUMN `name` varchar(16);
ALTER TABLE `students` MODIFY COLUMN `age` tinyint;

常见的gorm标签还包括:

标签 作用
type 设置字段的数据类型
size 设置字段的数据大小(按位数计算)
column 自定义字段的名称
primaryKey 设置主键
unique 设置唯一性
autoIncrement 数据自增
not null 不为空
default 设置缺省值
precision和scale 设置DECIMAL的精度和小数位数
comment 增添注释
check 增添CHECK约束

连用多个标签时,中间用;隔开。

三.单表操作

1.单表插入

单表插入的数据需要我们自行实例化一个结构体,并将其地址传入DB.Create()

1
2
3
4
5
6
7
8
s1 := Student{
		Name: "Outer",
		Age:  18,
	}
	err := DB.Create(&s1).Error
	if err != nil {
		fmt.Println(err)
	}

这样我们便得到的一段代码行

1
INSERT INTO `students` (`name`,`age`) VALUES ('Outer',18)

注意

同之前所述,若定义结构体时字段类型为指针,则默认空值NULL,若不为指针,则字符串对应的空值为'\0'(空字符串),整型对应的空值为0,且不能传nil值(指针类型可以)。

1
2
3
4
5
6
7
8
9
10
	s1 := Student{
		Name: nil,
		Age:  18,
	}
	err := DB.Create(&s1).Error
	if err != nil {
		fmt.Println(err)
	}
/*--------------------------------------------*/
ERROR:cannot use nil as string value in struct literal

DB.Create()可批量插入数据,我们可以选择传入一个切片的指针。

1
2
3
4
5
6
7
8
9
	slice := make([]Student, 0)
	for i := 0; i < 100; i++ {
		s1 := Student{
			Name: "Outer" + strconv.Itoa(i) + "号",
			Age:  18,
		}
		slice = append(slice, s1)
	}
	DB.Create(&slice)

2.单表查询

可以通过多种方法来查询单行数据

其中最简单的便是DB.FirstDB.last,通过对主键进行排序(升序),来获取第一行和最后一行的数据。

1
2
3
4
5
6
7
DB.First(&student)
fmt.Println(student)
DB.Last(&student)
fmt.Println(student)
//输出结果
{1 Outer0号 18}
{100 Outer99号 18}

此外,我们可以通过DB.Take()来进行查询

1
2
3
4
5
var student Student
DB.Take(&student)
fmt.Println(student)
//输出内容:
{1 Outer0号 18}

通过DB.Take()方法来获取该结构体对应的表的第一行数据

也可以向Take()内增添主键的值来查询对应主键的数据。

1
2
3
4
5
var student Student
DB.Take(&student, 84)
fmt.Println(student)
//输出
{84 Outer83号 18}

若超出查询范围,则会返回Record not found

此外也可以通过格式化查询来实现其他字段的查询操作。

1
2
3
4
DB.Take(&student, "name = ?", "Outer8号")
DB.Take(&student, fmt.Sprintf("name = '%s'","Outer8号"))
//对应SQL语句
SELECT * FROM Students WHERE name = 'Outer8号';

同样的,若Take()内的结构体有一个字段已确定(只能为主键),他会根据该主键字段的信息进行查询。

1
2
3
4
5
6
var student Student
student.ID = 56
DB.Take(&student)
fmt.Println(student)
//输出结果
{56 Outer55号 18}

通过增添RowAffected字段可以返回有多少行受到影响

1
count := DB.Take(&student).RowsAffected

DB.Find()可以实现数据的多行查询

1
2
3
4
5
6
7
8
9
var slice []Student
DB.Find(&slice, []int{45, 23, 54, 67})
fmt.Println(slice)
//或
var slice []Student
DB.Find(&slice, "name in (?)", []string{"Outer23号", "Outer87号"})
fmt.Println(slice)
//对应SQL语句
SELECT * FROM `students` WHERE name in ('Outer23号','Outer87号');

TakeFind的区别

1
2
3
4
5
DB.Take(&student, 2)
DB.Find(&student, 2)
//对应的SQL语句
SELECT * FROM `students` WHERE `students`.`id` = 2 LIMIT 1;
SELECT * FROM `students` WHERE `students`.`id` = 2;

二者均能将对应行的数据存入结构体,但Find也可以将数据存入切片

Find进行批量操作时,只能将切片的第一个值存入结构体,若传入的是切片,则可以将所有查询结果传入切片

1
2
3
4
5
6
7
8
9
DB.Find(&student, []int{1, 2, 3})
fmt.Println(student)
DB.Find(&slice, "ID in ?", []int{1, 2, 3})
fmt.Println(slice)
//对应的SQL语句
SELECT * FROM `students` WHERE `students`.`id` IN (1,2,3);
//输出数据
{1 Outer1号 18}
[{1 Outer1号 20} {2 Outer2号 20} {3 Outer3号 20}]

3.更新

我们可以通过Save操作来实现某个结构体数据的保存,将其绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
var student Student
DB.Take(&student, 11)
fmt.Println(student)
student.Age = 19

DB.Save(&student)
DB.Take(&student, 11)
fmt.Println(student)
//对应SQL语句
UPDATE `students` SET `name`='Outer11号',`age`=19 WHERE `id` = 11;
//返回值
{11 Outer10号 18}
{11 Outer10号 19}

但注意,Save是根据主键来修改的,当存在主键时,他会将主键对应的数据进行修改,当主键不存在时则在表的最后创建新的行

1
2
3
4
5
6
var student Student
student.Name = "Outer2号"
DB.Save(&student)
DB.Find(&student, 2)
//返回值
{103 Outer2号 0}

我们要选择某个特定字段来进行更改的话,可以使用Select语句

1
DB.Select("Name").Save(&student)

上述语句便只能将studentName字段进行更改。

此外,我们可以通过Update语句来对Find查询的数据进行更改

1
2
3
4
5
DB.Find(&student, 2).Update("age", 19)
//对应SQL语句
UPDATE `students` SET `age`=19 WHERE `students`.`id` = 2 AND `id` = 2;
//输出结果
{2 Outer2号 19}

与其对应的是Updates,可以对数据进行批量的更改

1
2
3
4
var slice []Student
DB.Find(&slice, "ID in ?", []int{1, 2, 3, 4, 5, 6, 7, 8}).Updates(Student{Age: 20})
//对应SQL语句
UPDATE `students` SET `age`=20 WHERE ID in (1,2,3,4,5,6,7,8) AND `id` IN (1,2,3,4,5,6,7,8)

此处的结构体Student{Age: 20}也可以通过映射map[string]interface{}{"Age": 20}来代替。

但该映射一定要求是map[string]interface{}map[string]any类型


UpdateUpdates的区别

1
2
3
4
5
6
7
8
9
10
11
12
//Update的源码
func (db *DB) Update(column string, value interface{}) (tx *DB) {
	tx = db.getInstance()
	tx.Statement.Dest = map[string]interface{}{column: value}
	return tx.callbacks.Update().Execute(tx)
}
//Updates的源码
func (db *DB) Updates(values interface{}) (tx *DB) {
	tx = db.getInstance()
	tx.Statement.Dest = values
	return tx.callbacks.Update().Execute(tx)
}

不难发现,Update将一个映射拆成了两部分,因此用Update传入值的时候格式为Update(string,any),且只能进行单个字段的更改

Updates则是只能接受结构体映射


4.删除

我们可以通过Delete语句进行删除操作

1
2
3
4
var student Student
DB.Delete(&student, 3)
//对应的SQL语句
DELETE FROM `students` WHERE `students`.`id` = 3;

同理,Delete也可以传入切片来实现批量删除

1
2
3
4
var student Student
DB.Delete(&student, []int{1, 2, 3, 4, 5, 6, 7, 8})
//对应的SQL语句
DELETE FROM `students` WHERE `students`.`id` IN (1,2,3,4,5,6,7,8);