泛型 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 | 持久化模型,最终会被查询或写入数据库 | User、Role、Flow |
TParams | 写操作参数类型,从 Request.Params 解码 | UserParams、CreateUserParams |
TSearch | 读操作搜索参数类型,从 Request.Params 解码 | UserSearch、RoleSearch |
各类 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] | create | post / | params 中的 TParams | 主键 map | 创建单条记录 |
NewUpdate[TModel, TParams] | update | put /:id | params 中的 TParams,且需要包含主键字段 | 成功结果 | 更新单条记录 |
NewDelete[TModel] | delete | delete /:id | params 中的原始主键值 | 成功结果 | 删除单条记录 |
NewCreateMany[TModel, TParams] | create_many | post /many | CreateManyParams[TParams],包含 list | 主键 map 列表 | 批量创建 |
NewUpdateMany[TModel, TParams] | update_many | put /many | UpdateManyParams[TParams],包含 list | 成功结果 | 批量更新 |
NewDeleteMany[TModel] | delete_many | delete /many | DeleteManyParams,包含 pks | 成功结果 | 批量删除 |
NewFindOne[TModel, TSearch] | find_one | get /:id | params 中的 TSearch | 单个模型 | 单条查询 |
NewFindAll[TModel, TSearch] | find_all | get / | params 中的 TSearch | []TModel | 不带分页元数据的列表查询 |
NewFindPage[TModel, TSearch] | find_page | get /page | params 中的 TSearch + meta 中的 page.Pageable | page.Page[T] | 后台分页列表 |
NewFindOptions[TModel, TSearch] | find_options | get /options | params 中的 TSearch + meta 中的 DataOptionConfig | []DataOption | 下拉选项 |
NewFindTree[TModel, TSearch](treeBuilder) | find_tree | get /tree | params 中的 TSearch | 分层 []TModel | 树形数据 |
NewFindTreeOptions[TModel, TSearch] | find_tree_options | get /tree/options | params 中的 TSearch + meta 中的 DataOptionConfig | []TreeDataOption | 树形选项 |
NewExport[TModel, TSearch] | export | get /export | params 中的 TSearch + meta 中的导出格式 | 文件下载 | Excel / CSV 导出 |
NewImport[TModel] | import | post /import | multipart 上传文件 + 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 | 作用于所有查询部分 |
对于 FindTree 和 FindTreeOptions,一些方法的默认作用范围和普通查询不同:
WithCondition(...)默认作用于QueryBaseWithQueryApplier(...)默认作用于QueryBaseWithSelect(...)、WithSelectAs(...)、WithRelation(...)默认同时作用于QueryBase和QueryRecursive
读操作 Builder
FindOne[TModel, TSearch]
当资源应返回一条记录时使用 FindOne。
| 维度 | 说明 |
|---|---|
| 泛型 | TModel 是查询目标模型,TSearch 定义筛选条件 |
| 输入 | params 中的 TSearch,以及 meta 中的原始 api.Meta |
| 输出 | 单个 TModel,或 WithProcessor(...) 转换后的结果 |
| 默认行为 | 查询模型列,并自动加上 LIMIT 1 |
| 常见配置 | WithCondition、WithRelation、WithQueryApplier、WithAuditUserNames |
当这个接口本质上仍然是“查询”,而不是“固定上下文的元数据读取”时,优先使用它。
FindAll[TModel, TSearch]
当你需要一个不带分页元数据的列表时使用 FindAll。
| 维度 | 说明 |
|---|---|
| 泛型 | TModel 是结果模型,TSearch 定义筛选条件 |
| 输入 | params 中的 TSearch,meta 中的 api.Meta |
| 输出 | []TModel,或者 WithProcessor(...) 返回的结果切片 |
| 默认行为 | 会应用安全上限 maxQueryLimit,并在空结果时返回空切片而不是 nil |
| 常见配置 | 共享 Find 配置,尤其是 WithDefaultSort、WithCondition、WithRelation、WithQueryApplier |
FindPage[TModel, TSearch]
绝大多数后台列表接口都应该从 FindPage 开始。
| 维度 | 说明 |
|---|---|
| 泛型 | TModel 是列表项模型,TSearch 定义查询条件 |
| 输入 | params 中的 TSearch,meta 中的 page.Pageable,以及额外的 api.Meta |
| 输出 | page.Page[T] |
| 默认行为 | 自动分页、统计总数,并规范化分页参数 |
| 特有配置 | WithDefaultPageSize(size) 用于设置默认页大小 |
当调用方需要 total、页码、页大小和列表项一起返回时,优先使用它。
FindOptions[TModel, TSearch]
当你需要轻量选项列表,例如下拉框时,使用 FindOptions。
| 维度 | 说明 |
|---|---|
| 泛型 | TModel 是来源模型,TSearch 定义筛选条件 |
| 输入 | params 中的 TSearch,meta 中的 DataOptionConfig |
| 输出 | []DataOption |
| 默认行为 | 将结果映射为 label、value、description 和可选 meta |
| 特有配置 | WithDefaultColumnMapping(mapping) 用于设置 label / value / description / meta 列映射的默认值 |
DataOptionConfig 来自 meta,可配置字段包括:
| 字段 | 作用 |
|---|---|
labelColumn | label 的来源列 |
valueColumn | value 的来源列 |
descriptionColumn | 可选的 description 来源列 |
metaColumns | 额外塞进 meta 对象的列 |
默认值:
labelColumn默认是namevalueColumn默认是id
FindTree[TModel, TSearch]
当领域数据本身是树形结构,且你希望返回嵌套模型时,使用 FindTree。
构造器形态和其他 Find builder 不同:
crud.NewFindTree[Category, CategorySearch](tree.Build)
| 维度 | 说明 |
|---|---|
| 泛型 | TModel 是树节点模型,TSearch 定义筛选条件 |
| 输入 | params 中的 TSearch,meta 中的 api.Meta |
| 输出 | 分层 []TModel |
| 默认行为 | 先构建递归 CTE 拉平查询结果,再交给 treeBuilder 组装成树 |
| 特有配置 | WithIDColumn(name)、WithParentIDColumn(name) |
默认值:
- 节点 ID 列默认为
id - 父节点列默认为
parent_id
FindTreeOptions[TModel, TSearch]
当你需要树形选项,而不是完整模型结构时,使用 FindTreeOptions。
| 维度 | 说明 |
|---|---|
| 泛型 | TModel 是来源模型,TSearch 定义筛选条件 |
| 输入 | params 中的 TSearch,meta 中的 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 中的 TSearch,meta 中的 format |
| 输出 | 文件下载 |
| 默认行为 | 先执行一个 Find 风格查询,再在可选预处理后导出 Excel / CSV |
| 特有配置 | WithDefaultFormat(...)、WithExcelOptions(...)、WithCsvOptions(...)、WithPreExport(...)、WithFilenameBuilder(...) |
format 可选值:
| 格式 | 值 |
|---|---|
| Excel | excel |
| CSV | csv |
默认值:
- 导出格式默认是
excel - 默认文件名分别是
data.xlsx和data.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 模型时,就该切到那里了。