最近准备上手一个新项目,然后在选择底层的 ORM 框架

我之前一直用的 GORM ,其实感觉还挺好用的,但是毕竟用多了,这次也想了解下其他的解决方案

之前在和群友讨论 GORM 的时候,我听见有人对它有一些负面的看法,比如说认为 GORM 不算真正的 ORM,因为它很多时候只是在帮你手动拼凑 SQL 语句,只是没有写原生 SQL 那么痛苦而已,我听了之后感觉其实也有点道理

比如说我最近项目里的一句查询

1
2
3
4
5
l.svcCtx.DBList.Mysql.
Where("from_id = ? and to_user_id = ?", in.UserAId, in.UserBId).
Or("from_id = ? and to_user_id = ?", in.UserBId, in.UserAId).
Order("created_at desc").
First(&result)

其中还是要手打 from_idto_user_id 这些字段名,就像原生 SQL 一样,而真正的 ORM 不应当是这样的,应当走如同 ent 这种代码生成的路子

甚至我也听过有人认为不应该使用 ORM 框架:为什么要旗帜鲜明地反对 orm 和 sql builder

也就是说你的 ORM 虽然方便,但是不方便进行 SQL 语句的审查,也就是说你不能预测线上环境会生成哪些 SQL,会有不确定性

但是其实我觉得吧,他说的场景我现在都还遇不到,我的项目也就是一些简单的 CURD ,而且数据量也不大

那么现在的情况就是说,我可以去尝试一下代码生成类型的 ORM ,例如 ent,也可以尝试其他方案,如 sqlx、sqlc 之类的

但是我真的离不开 GORM 的关联关系哇,真的是太方便了(我知道 ent 也有类似的设计,但我还是习惯 GORM 的)

然后我想起来 GORM 其实也推出了一个代码生成的版本,也就是 GEN 模式:https://gorm.io/zh_CN/gen/,之前在B站也刷到了 BV1Es4y1W7eg

我就打算尝试一下这个 GORM 的 GEN 模式(下面简称 GEN 了)


GEN 支持从数据库进行代码生成,也可以通过已经存在的 GORM model 生成代码

下面就尝试从 model 生成,就拿经典的老师学生一对多关系举例

首先把包拉一下

1
go get -u gorm.io/gen

在项目根目录新建 model文件夹,然后创建 model.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package model

type Student struct {
Id int
Name string

TeacherID int
}

type Teacher struct {
Id int
Name string

// has many
Student []Student
}

然后我们准备生成代码了,但是不是用一个命令生成,是用一个 golang 程序,里面包含了配置,我感觉这样的设计有点意思

退回项目根目录,新建 build文件夹,然后创建 build.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"gorm-gen/model"
"gorm.io/gen"
)

//// Dynamic SQL
//type Querier interface {
// // SELECT * FROM @@table WHERE name = @name{{if role !=""}} AND role = @role{{end}}
// FilterWithNameAndRole(name, role string) ([]gen.T, error)
//}

func main() {
g := gen.NewGenerator(gen.Config{
OutPath: "./query",
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
})

// gormdb, _ := gorm.Open(mysql.Open("root:@(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"))
//g.UseDB(gormdb) // reuse your gorm db

// Generate basic type-safe DAO API for struct `model.User` following conventions
g.ApplyBasic(model.Student{}, model.Teacher{})

// Generate Type Safe API with Dynamic SQL defined on Querier interface for `model.User` and `model.Company`
//g.ApplyInterface(func(Querier) {}, model.User{}, model.Company{})

// Generate the code
g.Execute()
}

GEN 还支持动态 SQL ,这东西我感觉我还用不上,就没折腾

执行之后,应该能生成 query 目录

1
2
3
4
5
6
7
8
9
10
11
.
├── build
│   └── main.go
├── go.mod
├── go.sum
├── model
│   └── model.go
└── query
├── gen.go
├── students.gen.go
└── teachers.gen.go

然后你就可以开始写 main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
"fmt"
"gorm-gen/model"
"gorm-gen/query"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)

func main() {
dsn := "root:12345678@tcp(127.0.0.1:3306)/gorm_learning?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
panic(err)
}
err = db.AutoMigrate(&model.Student{}, &model.Teacher{})
if err != nil {
panic(err)
}
query.SetDefault(db)

// 增
student1 := model.Student{Name: "student1"}
student2 := model.Student{Name: "student2"}
student3 := model.Student{Name: "student3"}
_ = query.Student.Create(&student1, &student2, &student3)

teacher1 := model.Teacher{Name: "teacher1"}
_ = query.Teacher.Create(&teacher1)

// 删
_, _ = query.Student.Where(query.Student.Id.Eq(3)).Delete()

// 改
_, _ = query.Student.Where(query.Student.Id.Eq(2)).Update(query.Student.Name, "student2_new")

// 查
student, _ := query.Student.Where(query.Student.Id.Eq(1)).Take()
teacher, _ := query.Teacher.Where(query.Teacher.Id.Eq(1)).Take()

fmt.Println(student) // {1 student1 0}
fmt.Println(teacher) // {1 teacher1 []}

// 关联
_ = query.Teacher.Student.Model(&teacher1).Append(&student1, &student2)
teacher, _ = query.Teacher.Preload(query.Teacher.Student).Where(query.Teacher.Id.Eq(1)).Take()

fmt.Println(teacher) // {1 teacher1 [{1 student1 1} {2 student2_new 1}]}
}

观察执行语句,你会发现与原版 GORM 的不同,比如说你在更新字段的时候,原版应该是这样写的

1
GLOBAL_DB.Model(&Student{}).Where("ID = ?", 2).Update("Name", "student2_new")

而现在变成了这样

1
query.Student.Where(query.Student.Id.Eq(2)).Update(query.Student.Name, "student2_new")

我感觉这样更能避免错误,看上去也更加安全,我这次项目就打算尝试一下用这个 GEN 模式了