GORM初探 | 青训营笔记

129 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

简介

GORM是基于go语言的ORM(Object-Relationl Mapping,即对象关系映射)操作库,使用简单,对开发者友好。

通过把struct结构体中的类型与数据库表中的列属性进行映射,完成通过go语言进行数据库中数据的操作。(具体如何完成映射见下面论述)

目前GORM支持的数据库有MySQL,SQLServer,PostgreSQL,SQLite。可以通过复用/自行开发驱动来连接其他类型的数据库。

MYSQL数据库基本操作

安装依赖

// 安装MySQL依赖
go get -u [gorm.io/driver/mysql](<http://gorm.io/driver/mysql>)
// 安装Gorm依赖
go get -u [gorm.io/gorm](<http://gorm.io/gorm>)

连接数据库

dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
	panic(err)
}

dsn的全称为Data Source Name,基本格式如下所示:username:password@protocol(address)/dbname?param=value

具体细节可见github.com/go-sql-driv…

连接好数据库后便可以操作数据表,我们可以自己创建数据表,也可以AutoMigrate()方法来刷新数据库中的表格定义,使其保持最新,如果没有对应的话,其会创建新的表。

AutoMigrate()

利用如下所示的代码来创建数据表

package main

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

type StudentInfo struct {
	StudentID int
	Name      string
	Grade     string
}

func main() {
	dsn := "root:sss041619@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	db.AutoMigrate(&StudentInfo{})
}

最终创建的表的格式如下图所示:

71.jpeg

至于为什么表名和列名是这样的,就不得不提法GORM的一些默认配置了

  • 表名默认为结构体名字的蛇形复数,也就是为什么表名是student_infos了
  • 默认结构体中字段名的蛇形作为列名
  • 默认结构体中的ID字段作为表中相应的主键

当然,我们可以手动配置表名和列名,这时结构体声明过程中需要添加如下配置:

type StudentInfo struct {
	ID        int
	StudentID int    `gorm:"primarykey"`
	Name      string `gorm:"column:student_names"`
	Grade     string `gorm:"column:student_grade"`
}

func (s StudentInfo) TableName() string {
	return "students"
}

最终的结果如下图所示:

72.jpeg

最后,我们还可以通过内嵌结构体的方式将gorm.model内嵌入自定义的结构体中,gorm.model包含的内容有如下几项,读者可以自己尝试

type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt DeletedAt `gorm:"index"`
}

自己创建数据表

首先通过MySQL语句创建一个数据表

CREATE TABLE if not exists message_info
( 
		message_id   bigint AUTO_INCREMENT NOT NULL COMMENT '自增主键,消息唯一标识',
    from_user_id  bigint  NOT NULL COMMENT '消息发出方',
    to_user_id  bigint  NOT NULL COMMENT '消息目标方',
    token   char(129) NOT NULL COMMENT '消息鉴权',
    content   varchar(255)NOT NULL COMMENT '消息内容',
    create_time  char(6) NOT NULL COMMENT '消息在服务器上创建时间',
    PRIMARY KEY (`message_id`),
    KEY (token) COMMENT'查询聊天记录用'
 )ENGINE = InnoDB
 DEFAULT charset = utf8mb4;

然后根据表中的各项信息,在go文件中定义结构体,如下图所示:

type MessageInfo struct {
	MessageId  int
	FromUserId int
	ToUserId   int
	Token      string
	Content    string
	CreatTime  string
}

func (mi MessageInfo) TableName() string {
	return "message_info"
}

无需AutoMigrate(),连接数据库后即可使用。

增加数据

操作students数据表,增加数据时使用的代码如下:

s1 := StudentInfo{ID: 1, Name: "zhangsan", Grade: "junior"}
res := db.Create(&s1)
fmt.Println(s1.StudentID)  // 1
fmt.Println(res.Error)     // nil

运行结果如下:

73.jpeg

虽然没有设置StudentID,但因为其是自增主键,因此会将其设为1,并且回填至s1。

也可以批量进行插入,代码如下:

s := []StudentInfo{{Name: "zhangsan_1"}, {Name: "zhangsan_2"}, {Name: "zhangsan_3"}}
db.Create(&s)

结果为:

74.jpeg

查询数据

可以使用First()函数查询,获取表中的第一条记录(主键升值)

var s2 StudentInfo
	db.First(&s2)
	fmt.Println(s2) //{1 1 zhangsan juni

相当于MySQL语句:SELECT * FROM students ORDER BY stddent_id LIMIT 1;

使用Last()获取最后一条记录:

var s2 StudentInfo
	db.Last(&s2)
	fmt.Println(s2) //{0 4 zhangsan_3 }

相当于MySQL语句SELECT * FROM students ORDER BY id DESC LIMIT 1;

此外,还可以使用Find()函数查找记录,对单个对象 db.Find(&s2) 将不加限制地使用 Find 将查询整个表并仅返回第一个性能不佳且不确定的对象,因此,使用Find()时要配以条件进行查询,当然,First函数也可以配以条件进行查询,不过如果找不到记录,First函数会返回ErrRecordNotFound,但是使用Find查询不到数据时不会返回错误。

var s2 StudentInfo
db.First(&s2, 1)              //student_id = 1
fmt.Println(s2)               //{1 1 zhangsan junior}
s2.StudentID = 0              //必须加,不然会 AND 上StudentID = 1的条件
db.First(&s2, []int{2, 3, 4}) //stdudent_id in {2,3,4}
fmt.Println(s2)               //{0 2 zhangsan_1 }
s2.StudentID = 0
res := db.First(&s2, 100) //student_id = 100
fmt.Println(res.Error)    //record not found
s2.StudentID = 0
db.Where("student_names = ?", "zhangsan_2").First(&s2) //Name = zhahhngsan_2
fmt.Println(s2)                                        //{0 3 zhangsan_2 }
s := make([]*StudentInfo, 0)
db.Where("student_names LIKE ?", "zhangsan_%").Find(&s)
for i, _ := range s {
	fmt.Println(s[i].Name, s[i].StudentID)
}
//zhangsan_1 2
// zhangsan_2 3
// zhangsan_3 4

在上面的范围查询函数中,我们最终展示了Name和StudentID两个属性,但是查询函数把所有的属性都返回给了s切片,这种情况下造成了资源浪费——

GORM 允许通过 [Select](<https://gorm.io/zh_CN/docs/query.html>)  方法选择特定的字段,如果您在应用程序中经常使用此功能,你也可以定义一个较小的结构体,以实现调用 API 时自动选择特定的字段。

type StudentInfo struct {
	ID        int
	StudentID int    `gorm:"primarykey"`
	Name      string `gorm:"column:student_names"`
	Grade     string `gorm:"column:student_grade"`
}
type API struct {
	StudentID int    `gorm:"primarykey"`
	Name      string `gorm:"column:student_names"`
}

func (s StudentInfo) TableName() string {
	return "students"
}
api := make([]*API, 0)
db.Model(&StudentInfo{}).Where("student_names LIKE ?", "zhangsan_%").Find(&api)
for i, _ := range api {
	fmt.Println(api[i].Name, api[i].StudentID)
}
//zhangsan_1 2
//zhangsan_2 3
//zhangsan_3 4

更新数据

Gorm更新数据是通过Update函数操作的,Update函数需要传入要更新的字段和对应的值。

db.Model(&StudentInfo{StudentID: 2}).Update("Name", "lisi")
//UPDATE students SET student_names = "lisi" WHERE student_id = 2

更新后的结果如下所示:

75.jpeg

删除数据

删除一条数据

删除一条数据时,删除对象必须指定主键,否则会触发批量删除。

db.Delete(&StudentInfo{},1)
s3 := StudentInfo{StudentID: 1}
db.Delete(&s3)
//DELETE FROM students WHERE student_id = 10;

删除多条数据

如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录。

当然也可以指定多个主键来删除对应的多条数据。

db.Where("stddent_names LIKE ?","zhangsan%").Delete(&StudentInfo{})
	//DELETE FROM students WHERE stddent_names LIKE "zhangsan%"";
	db.Delete(&StudentInfo{},[]int{1,2,3})
	//DELETE FROM students WHERE student_id in (1,2,3);

软删除

如果您的模型包含了一个 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力!

拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。

如果您不想引入 gorm.Model,您也可以在结构体中添加Deleted gorm.DeletedAt来启用软删除特性。