Appearance
基仓储
关于仓储
- 仓储是四层应用架构中最底层的数据访问层,它职责是处理数据库相关操作:把数据从 DB 搬到内存,或反过来,并提供数据表 Schema 元信息。
- 基仓储代码位于
internal/repository/base.go。
基仓储 repository.Repository[T] 封装了一些通用方法:
| 方法 | 说明 | 消费的选项 |
|---|---|---|
Create | 新增单条记录 | SelectFields、OmitFields |
Get | 按主键(或 scopes)查询单条记录,First | Scopes、Preloads、PrimaryKeyValue、SelectFields、OmitFields |
List | 查询数据列表,Find | Scopes、Preloads、SelectFields、OmitFields |
Count | 统计记录总数(使用 List 相同的筛选条件,但忽略分页) | Scopes |
Update | 按主键更新(map[string]any) | PrimaryKeyValue、SelectFields、OmitFields |
Delete | 按主键批量删除(IN ?) | PrimaryKeyValues |
PrimaryKeyField | 获取当前模型的主键数据库字段名 | |
FieldExists(field) | 检查当前模型是否包含指定字段 | |
Schema | 获取当前模型解析后的 GORM Schema | |
DB | 获取当前 DB 实例,优先返回自定义实例,否则返回全局实例 |
更新方法
更新方法比较特别,我们本来实现的是 struct 更新,可以用 GORM 的泛型 API,控制器绑定到结构体,然后直接更新入库,看起来一切很很好。
但是,结构体在更新环境下有一个坑,它并不是所见即所得的:结构体字段值默认是对应类型的零值 + GORM 的结构体更新会自动跳过零值;
比如你想将某字段值设为 0、false、time.Time{},走结构体更新默认是不行的,需要配合 Select() 才能生成对应的 SET 语句。
为了消除这种幻觉,以及方便前后端数据交互(结构体无法区分前端传递 NULL(本意是清空) 和 未传(本意是不更新)的情况,结构体这两种都接受为零值),我们选择直接将基仓储的 Update 改为 map 更新,它就是所见即所得了,没有心智负担;代价是不能使用泛型 API + 更新前可能需要手动组装 map 数据(如果使用 bindx.ShouldBindTri 函数绑定数据就不需要手动组装 map)。
选项
如下接口定义代码片段,您可以注意到很多方法都接受一个 opts Options(仓储层选项,服务层也有,请注意区分)。
go
// IRepository 通用仓库接口
type IRepository[T any] interface {
Create(ctx context.Context, entity *T, opts Options) error
Get(ctx context.Context, opts Options) (*T, error)
List(ctx context.Context, opts Options) ([]T, error)
Update(ctx context.Context, entity map[string]any, opts Options) error
}我们预设了一系列选项,每个方法所需要的选项都可以在此找到,但并非每个方法都会使用全部的选项;方法们直接接受整个 opts,而无需将选项逐一定义为方法参数(未来扩展参数也会方便很多)。
| 字段 | 类型 | 作用 |
|---|---|---|
Scopes | []func(*gorm.Statement) | 直接透传给 GORM Scopes |
OmitFields | []string | 排除出入库字段(黑名单),应用至 GORM Omit |
SelectFields | []string | 选择出入库字段(白名单),应用至 GORM Select |
PrimaryKeyValue | string | 主键值,按主键定位数据行 |
PrimaryKeyValues | []string | 主键切片,用于批量删除等 |
Preloads | []Preload | 预加载关联,传给 GORM Preload |
自定义 DB 实例
一般不需要自定义,会自动获取全局实例。
go
// 函数式选项自定义 DB 实例
repo := repository.NewRepository[model.Admin](repository.WithDB(customDB))
// 或者链式调用自定义 DB 实例
repo := repository.NewRepository[model.Admin]().WithDB(customDB)