gorm-note
关于 gorm 框架进行 mysql CRUD 的使用心得
还是直接写 sql 好,用代码生成或者泛型整一些单表简单操作,复杂操作用 sqlx 这样的 sql 框架写就好。
关于 ORM 框架,需要注意其利弊兼有的特点,其虽可以帮助我们简化一些简单 sql 操作,做一些辅助检查,但性能低,复杂查询表达不如直接使用 sql 的缺点是存在的。
对于简单查询,使用 ORM 框架是不错的,但综合性能和编写效率,复杂查询还是手写 sql 比较好。
综合一下还是觉得 java 中 MyBatis plus 负责简单查询,MyBatis 手写 sql 复杂查询的模式更好一些。
至于多表操作,其实有以下几个方案:
- 冗余字段或其他存储(redis 等):其实这个最快,如果可以通过冗余一两个字段完成操作最好。
- 本地多次单表查询:想提前返回错误或者要进行检查。
- join:性能其实相比本地多次单表查因为网络IO更快。但要注意较容易慢 sql。
实际业务进行选择,尽量避免三张表及以上的多表操作。
关于 join 还有很多优化策略,但这篇重点不是这个。
泛型
为了更好的使用 gorm,省去简单查询的重复编写是不可少的一步,可以使用 gorm-gen 负责生成代码,但有时会对生成的代码并不满意(比如方法实现上想做出调整),可以参照我下面结合泛型给出的例子:
单表查询的泛型,具体步骤如下:
定义类型限制的 interface,类似于下面这样
1 |
|
接着针对该 Model,定义相关的单表 Dao 接口和结构,方法实现
1 |
|
用 MDao 对 IDao 进行实现相关方法完成。
而对于具体使用而言,我们可以定义一个多表接口 Dao
1 |
|
上面只是个简单的例子,实际业务中还要看情况选择使用。
注意事项
建表规范
建表规范不同,其中要注意的几点是:
- 主键字段
- created_at,updated_at,deleted_at 等字段
- valid 字段
- CHARSET 是 utf8 还是 utf8mb4
- 注释
注意零值
注意数据库中不要规定任何值为 0 的数据为有实际意义的值,其作为数值使用的数值型类型,比如类似于 enum 使用的字段,因为如果在 gorm 的插入或修改中定义某值为 0,gorm 会认为该值不需要插入或修改,在某些情况下这样可能酿成大错。
如果想插入或更新零值,可以使用诸如 "0"
的做法。
对象返回
首先,永远不要返回双 nil,因为我们往往只检查 err。
对于对象的一对一关系,父子关系等等:
- 一对一:返回与主对象有父关系的所有数据,他们为对象提供了基本上下文
- 一对多:知道子关系数量有限时才返回子关系
列表或搜寻
我们需要实现一个 Filter 结构对结果进行过滤,分页等等
事务
应该确定的是,对逻辑而言,如果可以保证每一个函数调用都是一个独立的事务,世界会变得更美好。当然世界肯定不是完美的。
同时,应该设计事务平铺的机制以支持在 service 层操作被视为单独的事务。
方法命名
我个人使用的规范是:对于 ByA ,A 为 ID 时不加该后缀,对于增删改操作,后缀 Batch
代表批量进行
查找
Select
虽然查找上一般都能直接查找全部数据,但 Select
方法是减少查询时间下必须的。
子查询
1 |
|
查找或创建
使用 Attrs().FirstOrCreate()
或者使用 Assign().FirstOrCreate()
两者的区别是前者在找到后就不会使用 Attrs
中的属性,而后者即使找到也会使用 Assign
中的属性进行更新。
1 |
|
索引提示
对查询语句提示其应该使用哪个索引
1 |
|
迭代处理
使用迭代处理,可以比较轻易的实现树形结构查询等复杂查询情况
1 |
|
下面是一个树形结构查询例子
1 |
|
批量查询
使用 FindInBatches
进行批量查询,批量查询应该使用事务进行处理。
单列查询
使用 Pluck
方法进行单列查询进切片中
1 |
|
自定义查询函数
如分页函数等对 pageSize,page 等等进行检查,使用 Scopes
完成类似于方法的定义更有利于规范。
1 |
|
分页相关
首先,如果需要使用非唯一性索引分页排序,需要搭配唯一性索引,否则无法确保一致性。同时也需要确定非唯一性索引是否是联合索引。如果是,在中间以最左匹配原则加入联合索引的其他键可以进一步优化效率。
大分页下我们需要进行的查询如果使用非主键索引,需要进行多次回表效率极低,这里可以用延迟关联+覆盖索引实现高效分页查询
1 |
|
综合以上,分页在 gorm 的实现应该是这样的:
1 |
|
插入
插入可以分为几种类型,最简单的单次插入,以及批量插入,树形结构插入,插入或修改(upsert)等等。
单次插入
如下代码:
1 |
|
基本上只需要使用 Create
即可,这里也可以使用 Save
,其会将所有字段(包括零值字段)插入,但一般来说在良好的数据库定义和上文对零值的注意下并不需要,且对于某些比如 created_ad
等需要在数据库中使用 default 进行维护等字段,Save
就有些不实用了。
批量插入
使用 CreatedInBatched
方法。
创建或更新
我们经常需要预计到创建或更新的情况,即唯一性约束被触犯时,我们两种应对方法(返回或者更新原值),这两种都可以用 gorm
的 Clauses
方法解决,如下:
1 |
|
这里的 OnConflict
中有几种像 DoUpdates
这样的解决方案,分别是 DoUpdates
,DoNothing
和 UpdateAll
,后两种是布尔值,只需说明是否需要进行此过程即可,Columns
中是被冒犯到的唯一性约束。
实际上,Clauses
还有其他妙用。
树形结构插入
树形结构插入需要注意
- 子节点往往需要父节点的 ID 作为
parent_id
,这就意味着父节点的插入一定要在子节点之前。 - 对于一棵较大的树而言,我们需要用批量插入的形式来尽量减少 sql 的次数。这就意味着深度优先插入劣于广度优先(广度优先可以一次性返回)
也就时说,我们可以将树形结构插入分成这样的几个步骤:
- 定义插入过程,这里可以用匿名函数。
- 定义广度优先算法,更新当前层级和下一需要进行更新的层级。
- 使用事务,将上面的插入过程和广度优先更新层级的过程统合。
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// 定义插入过程
var create func(goods *[]model.Good) error
create = func(goods *[]model.Auth) error {
// 插入过程
}
// 定义广度优先过程
nextLevel := make([]model.GoodTree, 0)
creates := make([]model.Good, 0)
var getLevel func(goodtree *[]model.GoodTree)
getLevel = func(goods *[]model.GoodTree) {
nextLevel = make([]model.GoodTree, 0)
creates = make([]model.Good, 0)
for _, v := range *goodtree {
creates = append(creates, v.Value)
for _, nv := range v.Children {
// 要注意的是这里因为插入完成所以 v.Value.ID 在这时候就存在了
nv.Value.ParentID = v.Value.ID
nextLevel = append(nextLevel, *nv)
}
}
}
// 使用事务进行实际更新
tx := ds.DB.WithContext(ctx).Begin()
if tx.Error != nil {
return errors.WithStack(tx.Error)
}
getLevel(data)
for len(creates) > 0 {
err = create(&creates)
if err != nil {
tx.Rollback()
return errors.WithStack(err)
}
getLevel(&nextLevel)
}
return tx.Commit().Error
删除
一般都不真删除而是 update 逻辑删除相关的字段。
逻辑删除
逻辑删除基本是实际业务中必备的。
逻辑删除时针对可能影响唯一性约束时,可以有三种解决方案:
- 将删除标记设置默认值(例如 0 ),将唯一字段与删除标记添加唯一键约束。当某一记录需要删除时,将删除标记置为 NULL。
由于 NULL 不会和其他字段有组合唯一键的效果,所以当记录被删除时(删除标记被置为 NULL 时),解除了唯一键的约束。此外该方法能很好地解决批量删除的问题(只要置为 NULL 就完事了),消耗的空间也并不多( 1 位 + 联合索引)。 但 NULL 本身就可能出现问题。 - 将删除标记更新为主键,这样同样保证了唯一约束字段和删除标记组合索引的唯一性。
- 引入
deleted_at
字段,和唯一字段组成联合唯一索引,进行逻辑删除时同步更新。
最后一种解决方式基本是可以保证不会出现什么问题的
引入 gorm 自带的软删除机制通过查找被软删除的记录可以通过1
2
3
4
5type User struct {
ID int Deleted
gorm.DeletedAt
Name string
}Unscoped
解决
在上文中提到的最后一种和唯一字段组成联合唯一索引的方法,可以通过如下代码进行支持1
2
3
4
5
6
7import "gorm.io/plugin/soft_delete"
type User struct {
ID uint
Name string `gorm:"uniqueIndex:udx_name"`
DeletedAt soft_delete.DeletedAt `gorm:"uniqueIndex:udx_name"`
}gorm.io/plugin/soft_delete
还提供了上文的删除标记方法:1
2
3
4
5
6
7import "gorm.io/plugin/soft_delete"
type User struct {
ID uint
Name string
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}