Skip to content

基仓储

关于仓储

  1. 仓储是四层应用架构中最底层的数据访问层,它职责是处理数据库相关操作:把数据从 DB 搬到内存,或反过来,并提供数据表 Schema 元信息。
  2. 基仓储代码位于 internal/repository/base.go

基仓储 repository.Repository[T] 封装了一些通用方法:

方法说明消费的选项
Create新增单条记录SelectFieldsOmitFields
Get按主键(或 scopes)查询单条记录,FirstScopesPreloadsPrimaryKeyValueSelectFieldsOmitFields
List查询数据列表,FindScopesPreloadsSelectFieldsOmitFields
Count统计记录总数(使用 List 相同的筛选条件,但忽略分页)Scopes
Update按主键更新(map[string]anyPrimaryKeyValueSelectFieldsOmitFields
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
PrimaryKeyValuestring主键值,按主键定位数据行
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)