跳到主要内容

泛型 CRUD

crud 包是 VEF 最重要的用户层能力之一。它把类型化模型和类型化请求结构体变成可复用的 API 操作,并内置事务、验证、数据权限、文件提升和结果格式化。

基本模式

最常见的写法,是把 CRUD provider 直接嵌入资源结构体:

type UserResource struct {
api.Resource

crud.FindPage[User, UserSearch]
crud.Create[User, UserParams]
crud.Update[User, UserParams]
crud.Delete[User]
}

func NewUserResource() api.Resource {
return &UserResource{
Resource: api.NewRPCResource("sys/user"),
FindPage: crud.NewFindPage[User, UserSearch]().PermToken("sys:user:query"),
Create: crud.NewCreate[User, UserParams]().PermToken("sys:user:create"),
Update: crud.NewUpdate[User, UserParams]().PermToken("sys:user:update"),
Delete: crud.NewDelete[User]().PermToken("sys:user:delete"),
}
}

框架之所以能自动收集这些 CRUD builder,是因为它们本身实现了 api.OperationsProvider

泛型参数含义

大多数 CRUD builder 只会用到下面这几种泛型形状:

泛型含义常见类型
TModel持久化模型,最终会被查询或写入数据库UserRoleFlow
TParams写操作参数类型,从 Request.Params 解码UserParamsCreateUserParams
TSearch读操作搜索参数类型,从 Request.Params 解码UserSearchRoleSearch

各类 builder 对这些泛型的使用方式如下:

Builder 家族泛型形状含义
单条写操作Create[TModel, TParams]Update[TModel, TParams]TParams 会被拷贝进模型再持久化
批量写操作CreateMany[TModel, TParams]UpdateMany[TModel, TParams]框架会把 TParams 包装进批量 params 类型
读操作FindOne[TModel, TSearch]FindPage[TModel, TSearch]TModel 定义查询目标,TSearch 定义查询条件
删除操作Delete[TModel]DeleteMany[TModel]删除根据主键输入工作,不需要额外的 TParams
导出Export[TModel, TSearch]导出先执行读查询,再把结果渲染成文件
导入Import[TModel]文件中的行会直接解码成模型

预置 Builder 总览

Builder默认 RPC action默认 REST action输入契约输出契约典型用途
NewCreate[TModel, TParams]createpost /params 中的 TParams主键 map创建单条记录
NewUpdate[TModel, TParams]updateput /:idparams 中的 TParams,且需要包含主键字段成功结果更新单条记录
NewDelete[TModel]deletedelete /:idparams 中的原始主键值成功结果删除单条记录
NewCreateMany[TModel, TParams]create_manypost /manyCreateManyParams[TParams],包含 list主键 map 列表批量创建
NewUpdateMany[TModel, TParams]update_manyput /manyUpdateManyParams[TParams],包含 list成功结果批量更新
NewDeleteMany[TModel]delete_manydelete /manyDeleteManyParams,包含 pks成功结果批量删除
NewFindOne[TModel, TSearch]find_oneget /:idparams 中的 TSearch单个模型单条查询
NewFindAll[TModel, TSearch]find_allget /params 中的 TSearch[]TModel不带分页元数据的列表查询
NewFindPage[TModel, TSearch]find_pageget /pageparams 中的 TSearch + meta 中的 page.Pageablepage.Page[T]后台分页列表
NewFindOptions[TModel, TSearch]find_optionsget /optionsparams 中的 TSearch + meta 中的 DataOptionConfig[]DataOption下拉选项
NewFindTree[TModel, TSearch](treeBuilder)find_treeget /treeparams 中的 TSearch分层 []TModel树形数据
NewFindTreeOptions[TModel, TSearch]find_tree_optionsget /tree/optionsparams 中的 TSearch + meta 中的 DataOptionConfig[]TreeDataOption树形选项
NewExport[TModel, TSearch]exportget /exportparams 中的 TSearch + meta 中的导出格式文件下载Excel / CSV 导出
NewImport[TModel]importpost /importmultipart 上传文件 + meta 中的导入格式{total: n}Excel / CSV 导入

共享 Builder 配置

每个 CRUD builder 都继承了 Builder[T] 的通用控制项:

方法作用
ResourceKind(kind)切换为 RPC 或 REST 命名/校验规则
Action(action)覆盖默认 action 名
Public()将该操作标记为无需认证
PermToken(token)要求调用方具备某个权限点
Timeout(duration)设置请求超时
EnableAudit()为该操作启用审计记录
RateLimit(max, period)对该操作单独配置限流

一个容易忽略的点:

  • Action(...) 会按当前 ResourceKind(...) 进行校验
  • 如果你要覆盖 REST action,应该先调用 ResourceKind(api.KindREST)

Find 系列共享配置

所有读操作 builder 都建立在 Find[...] 之上,因此它们共享更丰富的查询塑形能力:

方法作用
WithProcessor(...)在响应序列化前,对查询结果做后处理
WithOptions(...)追加底层 FindOperationOption
WithSelect(column)SELECT 增加一列
WithSelectAs(column, alias)SELECT 增加一列并起别名
WithDefaultSort(...)当请求里没有动态排序时,设置默认排序
WithCondition(...)orm.ConditionBuilder 增加 WHERE 条件
DisableDataPerm()禁用默认的数据权限过滤
WithRelation(...)通过 orm.RelationSpec 增加关联 join
WithAuditUserNames(userModel, nameColumn...)自动 join 审计用户,补齐创建人/更新人名字
WithQueryApplier(...)用类型安全的方式直接修改查询对象

绝大多数 Find builder 的运行时默认行为:

  • 会自动应用 TSearch 上的 search:"..." 标签
  • 默认启用数据权限过滤
  • 如果模型有单主键,默认按主键倒序排序
  • 如果没有单主键,但有 created_at,则回退为 created_at DESC

Tree Builder 的 QueryPart

树形 builder 使用递归 CTE,因此部分配置项可以作用在不同查询阶段:

QueryPart含义
QueryRoot最外层最终查询
QueryBase递归 CTE 的起始查询
QueryRecursive递归 CTE 的递归分支
QueryAll作用于所有查询部分

对于 FindTreeFindTreeOptions,一些方法的默认作用范围和普通查询不同:

  • WithCondition(...) 默认作用于 QueryBase
  • WithQueryApplier(...) 默认作用于 QueryBase
  • WithSelect(...)WithSelectAs(...)WithRelation(...) 默认同时作用于 QueryBaseQueryRecursive

读操作 Builder

FindOne[TModel, TSearch]

当资源应返回一条记录时使用 FindOne

维度说明
泛型TModel 是查询目标模型,TSearch 定义筛选条件
输入params 中的 TSearch,以及 meta 中的原始 api.Meta
输出单个 TModel,或 WithProcessor(...) 转换后的结果
默认行为查询模型列,并自动加上 LIMIT 1
常见配置WithConditionWithRelationWithQueryApplierWithAuditUserNames

当这个接口本质上仍然是“查询”,而不是“固定上下文的元数据读取”时,优先使用它。

FindAll[TModel, TSearch]

当你需要一个不带分页元数据的列表时使用 FindAll

维度说明
泛型TModel 是结果模型,TSearch 定义筛选条件
输入params 中的 TSearchmeta 中的 api.Meta
输出[]TModel,或者 WithProcessor(...) 返回的结果切片
默认行为会应用安全上限 maxQueryLimit,并在空结果时返回空切片而不是 nil
常见配置共享 Find 配置,尤其是 WithDefaultSortWithConditionWithRelationWithQueryApplier

FindPage[TModel, TSearch]

绝大多数后台列表接口都应该从 FindPage 开始。

维度说明
泛型TModel 是列表项模型,TSearch 定义查询条件
输入params 中的 TSearchmeta 中的 page.Pageable,以及额外的 api.Meta
输出page.Page[T]
默认行为自动分页、统计总数,并规范化分页参数
特有配置WithDefaultPageSize(size) 用于设置默认页大小

当调用方需要 total、页码、页大小和列表项一起返回时,优先使用它。

FindOptions[TModel, TSearch]

当你需要轻量选项列表,例如下拉框时,使用 FindOptions

维度说明
泛型TModel 是来源模型,TSearch 定义筛选条件
输入params 中的 TSearchmeta 中的 DataOptionConfig
输出[]DataOption
默认行为将结果映射为 labelvaluedescription 和可选 meta
特有配置WithDefaultColumnMapping(mapping) 用于设置 label / value / description / meta 列映射的默认值

DataOptionConfig 来自 meta,可配置字段包括:

字段作用
labelColumnlabel 的来源列
valueColumnvalue 的来源列
descriptionColumn可选的 description 来源列
metaColumns额外塞进 meta 对象的列

默认值:

  • labelColumn 默认是 name
  • valueColumn 默认是 id

FindTree[TModel, TSearch]

当领域数据本身是树形结构,且你希望返回嵌套模型时,使用 FindTree

构造器形态和其他 Find builder 不同:

crud.NewFindTree[Category, CategorySearch](tree.Build)
维度说明
泛型TModel 是树节点模型,TSearch 定义筛选条件
输入params 中的 TSearchmeta 中的 api.Meta
输出分层 []TModel
默认行为先构建递归 CTE 拉平查询结果,再交给 treeBuilder 组装成树
特有配置WithIDColumn(name)WithParentIDColumn(name)

默认值:

  • 节点 ID 列默认为 id
  • 父节点列默认为 parent_id

FindTreeOptions[TModel, TSearch]

当你需要树形选项,而不是完整模型结构时,使用 FindTreeOptions

维度说明
泛型TModel 是来源模型,TSearch 定义筛选条件
输入params 中的 TSearchmeta 中的 DataOptionConfig
输出[]TreeDataOption
默认行为通过递归 CTE 取到树节点,再转换成 TreeDataOption 嵌套结构
特有配置WithDefaultColumnMapping(...)WithIDColumn(...)WithParentIDColumn(...)

当客户端只需要 label / value / children 这类树形选项载荷时,优先用它,而不是直接暴露完整模型。

写操作 Builder

Create[TModel, TParams]

单条创建时使用 Create

维度说明
泛型TModel 是持久化模型,TParams 是写入参数类型
输入params 中的 TParams
输出新建记录的主键 map
默认行为把 params 拷贝进新模型,处理文件提升,在事务内插入记录
特有配置WithPreCreate(...)WithPostCreate(...)

Hook 的作用分别是:

方法执行时机常见用途
WithPreCreate插入前,且仍在同一事务内归一化、校验、补默认值、追加查询控制
WithPostCreate插入后,且仍在同一事务内同事务内的业务联动、副作用

Update[TModel, TParams]

单条更新时使用 Update

维度说明
泛型TModel 是持久化模型,TParams 是写入参数类型
输入params 中的 TParams,且必须包含主键字段
输出成功结果
默认行为先把 params 拷贝到临时模型,校验主键,加载旧模型,应用数据权限,合并非空字段,再在事务中更新
特有配置WithPreUpdate(...)WithPostUpdate(...)DisableDataPerm()

一个重要细节:

  • Update 合并新值时使用的是 copier.WithIgnoreEmpty(),也就是空值字段不会覆盖旧值

Delete[TModel]

单条删除时使用 Delete

维度说明
泛型TModel 是持久化模型
输入params 中的原始主键值
输出成功结果
默认行为校验主键输入,加载模型,应用数据权限,在事务中删除,并在成功后清理已提升文件
特有配置WithPreDelete(...)WithPostDelete(...)DisableDataPerm()

批量 Builder

CreateMany[TModel, TParams]

维度说明
输入契约CreateManyParams[TParams],字段名是 list
输出主键 map 列表
特有配置WithPreCreateMany(...)WithPostCreateMany(...)
默认行为把每个 params 项拷贝为模型,并在一个事务里完成批量插入

UpdateMany[TModel, TParams]

维度说明
输入契约UpdateManyParams[TParams],字段名是 list
输出成功结果
特有配置WithPreUpdateMany(...)WithPostUpdateMany(...)DisableDataPerm()
默认行为校验每项主键、加载所有旧模型、合并更新值,并在一个事务里执行批量更新

DeleteMany[TModel]

维度说明
输入契约DeleteManyParams,字段名是 pks
输出成功结果
特有配置WithPreDeleteMany(...)WithPostDeleteMany(...)DisableDataPerm()
默认行为对单主键模型接受标量值,对复合主键模型接受 map,并在一个事务里完成批量删除

DeleteManyParams.pks 的输入规则:

主键形态允许的 payload 形态
单主键["id1", "id2"]
复合主键[{"user_id":"u1","role_id":"r1"}]

导出与导入 Builder

Export[TModel, TSearch]

当你需要把查询结果下载成 Excel 或 CSV 文件时,使用 Export

维度说明
泛型TModel 是导出行模型,TSearch 是查询条件类型
输入params 中的 TSearchmeta 中的 format
输出文件下载
默认行为先执行一个 Find 风格查询,再在可选预处理后导出 Excel / CSV
特有配置WithDefaultFormat(...)WithExcelOptions(...)WithCsvOptions(...)WithPreExport(...)WithFilenameBuilder(...)

format 可选值:

格式
Excelexcel
CSVcsv

默认值:

  • 导出格式默认是 excel
  • 默认文件名分别是 data.xlsxdata.csv

Import[TModel]

当调用方上传 CSV 或 Excel 文件,并希望把行数据解析后插入数据库时,使用 Import

维度说明
泛型TModel 是导入后要持久化的模型类型
输入multipart 上传文件 params.file,以及 meta 中可选的 format
输出成功时返回 {total: n}
默认行为强制要求 multipart 请求,把文件解析成模型,校验导入结果,并在事务中写入
特有配置WithDefaultFormat(...)WithExcelOptions(...)WithCsvOptions(...)WithPreImport(...)WithPostImport(...)

重要细节:

  • Import 不接受 JSON 请求
  • 如果导入校验失败,响应会返回 errors 载荷,而不是部分写入
  • 导入格式默认是 excel

实践建议

  • 后台资源优先从 FindPage + Create + Update + Delete 开始
  • 写 params 和 search params 必须分开
  • 权限控制尽量加在 builder 层,而不是散落到 handler 内部
  • 除非你非常清楚为什么要关闭,否则优先保留默认数据权限过滤
  • 下拉框或轻量树选项优先使用 FindOptions / FindTreeOptions,不要复用完整模型查询
  • 除非业务动作有更强的领域语义,否则优先沿用标准 CRUD 词汇
  • 一个资源里混合 CRUD builder 和少量自定义 action,是很常见也很合理的做法

下一步

继续看 自定义 Handler,当你的业务动作超出通用 CRUD 模型时,就该切到那里了。