一、背景与问题
Oinone 的树形下拉组件(TreeSelect)和树视图(Tree)在查询数据时,最终调用的是树节点所配置模型的数据管理器函数:
| 树接口 | 调用的数据管理器函数 | 场景 |
|---|---|---|
fetchAll |
queryListByWrapper |
一次性全量加载 |
fetchChildren |
queryPage |
按层级懒加载(默认) |
queryKeywords4Tree |
queryListByWrapper |
关键字搜索 |
reverselyQuery |
queryListByWrapper |
已选数据回填 |
官方文档原文:「所有树查询接口都会调用模型对应的数据管理器函数,可以通过重写的方式修改其查询结果。」
直接覆写的风险
如果直接在原模型(如 business.PamirsDepartment)上覆写 queryPage:
// ⚠️ 危险写法:会影响所有使用 PamirsDepartment.queryPage 的场景
@Component
@Model.model(PamirsDepartment.MODEL_MODEL)
public class DeptQueryAction {
@Function.Advanced(type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE)
@Function.fun(FunctionConstants.queryPage)
@Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API})
public Pagination<PamirsDepartment> queryPage(Pagination<PamirsDepartment> page, IWrapper<PamirsDepartment> queryWrapper) {
// 追加过滤条件...
return new PamirsDepartment().queryPage(page, queryWrapper);
}
}
该覆写会全局生效——不仅是树形下拉,所有调用 PamirsDepartment 的 queryPage 的地方(组织架构页面、人员管理列表、权限分配、框架内部关联查询等)都会受到影响,容易引发难以排查的副作用。
二、推荐方案:代理模型(PROXY)隔离
核心思路
- 创建一个继承原模型的代理模型(
ModelTypeEnum.PROXY),拥有独立的模型命名空间 - 在代理模型中重新声明自关联字段(如
parent),将关联目标指向代理模型自身 - 在代理模型的 Action 中覆写数据管理器函数,追加树形场景专属的过滤逻辑
- 树形下拉的视图 DSL 中,
<node>的model指向代理模型
代理模型与原模型共享同一张数据表,但函数注册在各自独立的命名空间下,互不干扰。
框架官方示例
Oinone 框架自身就采用了这一模式:ResourceRegionProxyModel 继承 ResourceRegion,用于地区页面的代理展示,其扩展点 ResourceRegionProxyModelExtPoint 中的 queryPageAfter 只影响代理模型自身的查询。
三、完整实现步骤(以 PamirsDepartment 为例)
场景假设
业务需求:某个表单页面的「部门」树形下拉只需要展示当前公司(companyCode == 当前用户公司)且启用状态(dataStatus == ENABLED)的部门,而系统中其他使用 PamirsDepartment 的地方(组织架构、权限管理等)不受影响。
步骤 1:定义代理模型
package com.example.demo.model;
import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.enmu.ModelTypeEnum;
import pro.shushi.pamirs.business.api.model.PamirsDepartment;
import java.util.List;
/**
* 部门树形下拉专用代理模型
* <p>
* 继承 PamirsDepartment,共享同一张数据表,
* 但拥有独立的函数命名空间,覆写 queryPage 不影响原模型。
*/
@Model.model(DeptTreeProxy.MODEL_MODEL)
@Model.Advanced(type = ModelTypeEnum.PROXY)
@Model(displayName = "部门(树选择)", labelFields = "name")
public class DeptTreeProxy extends PamirsDepartment {
public static final String MODEL_MODEL = "demo.DeptTreeProxy";
/**
* 【必须重新声明】上级部门(自关联字段)
* <p>
* 基类 PamirsDepartment 中 parent 的类型是 PamirsDepartment,
* 代理模型必须将其覆盖为指向自身(DeptTreeProxy),
* 否则设计器/树组件无法在代理模型命名空间内构成完整的自关联关系,
* 导致树形下拉无法正确展开层级。
*/
@Field.many2one
@Field.Relation(relationFields = {"parentCode"}, referenceFields = {"code"})
@Field(displayName = "上级部门")
private DeptTreeProxy parent;
/**
* 【必须重新声明】下级部门(自关联 O2M 字段)
* <p>
* 与 parent 对应,保持自关联关系的双向完整性。
*/
@Field.one2many
@Field.Relation(relationFields = {"code"}, referenceFields = {"parentCode"})
@Field(displayName = "下级部门")
private List<DeptTreeProxy> childList;
}
要点说明:
ModelTypeEnum.PROXY:标记为代理模型,不会新建数据表,底层复用父模型的表MODEL_MODEL使用项目自己的命名空间(如demo.),与框架的business.隔离
⚠️ 关键陷阱:自关联字段必须重新声明
基类
PamirsDepartment中parent字段的类型是PamirsDepartment,其关联关系指向business.PamirsDepartment。
如果代理模型不重新声明parent,则树组件在解析selfReferences="parent"时,
发现该字段的关联目标不是当前代理模型(demo.DeptTreeProxy),而是父模型(business.PamirsDepartment),
导致设计器页面和树形下拉无法构成完整的自关联树结构,表现为:树只有一层、无法展开子节点、或报错。因此,所有构成树形结构的自关联字段(M2O 的
parent和 O2M 的childList)都必须在代理模型中重新声明,将类型指向代理模型自身。
步骤 2:在代理模型 Action 中覆写数据管理器函数
package com.example.demo.action;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.connectors.data.sql.query.LambdaQueryWrapper;
import pro.shushi.pamirs.framework.connectors.data.sql.query.QueryWrapper;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.api.dto.condition.Pagination;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.constant.FunctionConstants;
import pro.shushi.pamirs.meta.enmu.FunctionCategoryEnum;
import pro.shushi.pamirs.meta.enmu.FunctionOpenEnum;
import pro.shushi.pamirs.meta.enmu.FunctionTypeEnum;
import pro.shushi.pamirs.core.common.enmu.DataStatusEnum;
import com.example.demo.model.DeptTreeProxy;
import java.util.List;
/**
* 部门树形下拉代理模型的查询动作
* <p>
* 覆写 queryPage 和 queryListByWrapper,
* 仅对树形下拉组件的查询生效,不影响 PamirsDepartment 原模型。
*/
@Component
@Model.model(DeptTreeProxy.MODEL_MODEL)
public class DeptTreeProxyAction {
/**
* 覆写分页查询 —— 树组件懒加载(fetchChildren)时调用
*
* @param page 分页参数
* @param queryWrapper 框架自动构建的查询条件(已包含树层级关联条件)
* @return 过滤后的分页结果
*/
@Function.Advanced(type = FunctionTypeEnum.QUERY, category = FunctionCategoryEnum.QUERY_PAGE)
@Function.fun(FunctionConstants.queryPage)
@Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API})
public Pagination<DeptTreeProxy> queryPage(Pagination<DeptTreeProxy> page,
QueryWrapper<DeptTreeProxy> queryWrapper) {
// 将 QueryWrapper 转为 LambdaQueryWrapper,通过方法引用指定字段,避免硬编码列名
LambdaQueryWrapper<DeptTreeProxy> wrapper = queryWrapper.lambda();
// 追加业务过滤条件:只展示启用状态的部门
wrapper.eq(DeptTreeProxy::getDataStatus, DataStatusEnum.ENABLED);
// 追加业务过滤条件:只展示当前用户所属公司的部门
String companyCode = PamirsSession.getContext().getCompanyCode();
wrapper.eq(DeptTreeProxy::getCompanyCode, companyCode);
// 调用代理模型的默认数据管理器执行查询
return new DeptTreeProxy().queryPage(page, wrapper);
}
/**
* 覆写列表查询 —— 树组件全量加载(fetchAll)和搜索(queryKeywords4Tree)时调用
*
* @param queryWrapper 框架自动构建的查询条件
* @return 过滤后的列表结果
*/
@Function.Advanced(type = FunctionTypeEnum.QUERY)
@Function.fun(FunctionConstants.queryListByWrapper)
@Function(openLevel = {FunctionOpenEnum.LOCAL, FunctionOpenEnum.REMOTE, FunctionOpenEnum.API})
public List<DeptTreeProxy> queryListByWrapper(QueryWrapper<DeptTreeProxy> queryWrapper) {
// 将 QueryWrapper 转为 LambdaQueryWrapper
LambdaQueryWrapper<DeptTreeProxy> wrapper = queryWrapper.lambda();
// 追加与 queryPage 相同的业务过滤条件
wrapper.eq(DeptTreeProxy::getDataStatus, DataStatusEnum.ENABLED);
String companyCode = PamirsSession.getContext().getCompanyCode();
wrapper.eq(DeptTreeProxy::getCompanyCode, companyCode);
return new DeptTreeProxy().queryListByWrapper(wrapper);
}
}
要点说明:
@Function.fun(FunctionConstants.queryPage):将方法注册为代理模型命名空间下的queryPage数据管理器函数,覆盖默认实现- 树组件内部通过
Models.data().queryPage(pagination, queryWrapper)调用时,pagination.model已被设置为节点配置的模型编码,因此会自动路由到代理模型的覆写版本 - 通过
queryWrapper.lambda()转为LambdaQueryWrapper,使用DeptTreeProxy::getDataStatus等方法引用指定字段,避免硬编码数据库列名,字段重命名时编译期即可发现错误 - 方法体内调用
new DeptTreeProxy().queryPage(page, wrapper)执行实际的数据库查询
步骤 3:配置树形下拉视图 DSL
在表单视图中,将 TreeSelect 字段的 <node> 指向代理模型:
<view model="业务模型编码" type="form" name="xxx_form_view">
<template slot="form">
<!-- 树形下拉字段:node 的 model 指向代理模型 -->
<field data="department" label="部门" widget="TreeSelect" span="1">
<nodes>
<node label="activeRecord.name"
model="demo.DeptTreeProxy"
selfReferences="parent"
searchFields="name" />
</nodes>
</field>
</template>
</view>
DSL 关键属性:
| 属性 | 说明 |
|---|---|
model |
树节点数据来源模型 —— 指向代理模型,这是隔离生效的关键 |
selfReferences |
自关联字段名(parent),表示该模型通过 M2O 字段 parent 构成树 |
label |
节点显示文本的表达式,activeRecord.name 表示取记录的 name 字段 |
searchFields |
搜索时匹配的字段列表 |
filter |
可选,RSQL 语法的静态过滤条件(简单场景可替代后端覆写) |
步骤 4(可选):多级树节点配置
如果树形下拉需要「公司 → 部门」两级结构:
<field data="department" label="部门" widget="TreeSelect" span="1">
<nodes>
<!-- 第一级:公司 -->
<node label="activeRecord.name" model="business.PamirsCompany" />
<!-- 第二级:部门,通过 company 字段关联上级,parent 构成部门内自关联 -->
<node label="activeRecord.name"
model="demo.DeptTreeProxy"
references="company"
selfReferences="parent"
searchFields="name" />
</nodes>
</field>
四、方案对比
| 维度 | 直接覆写原模型 | 代理模型隔离(推荐) | DSL filter 属性 |
|---|---|---|---|
| 影响范围 | 全局所有使用该模型查询的场景 | 仅树组件(指向代理模型的视图) | 仅该视图的树节点 |
| 灵活性 | 高(可写任意 Java 逻辑) | 高(可写任意 Java 逻辑) | 低(仅支持 RSQL 静态条件) |
| 副作用风险 | 高 | 无 | 无 |
| 适用场景 | 确实需要全局修改查询行为 | 特定页面/组件需要定制数据 | 简单的静态过滤 |
五、注意事项
- 自关联字段必须在代理模型中重新声明:基类的
parent(M2O)和childList(O2M)字段的关联目标指向基类模型,代理模型必须用自身类型重新声明这些字段,否则设计器无法识别代理模型内的完整树形关联关系,树形下拉将无法正确展开 - queryPage 和 queryListByWrapper 都要覆写:树组件的懒加载走
queryPage,全量加载和搜索走queryListByWrapper,只覆写一个会导致部分场景过滤失效 - 代理模型不建新表:
ModelTypeEnum.PROXY类型的模型复用父模型的物理表,无需担心数据同步问题 - 使用 LambdaQueryWrapper + 方法引用指定字段:通过
queryWrapper.lambda()转换后,使用DeptTreeProxy::getDataStatus等方法引用指定字段,避免硬编码数据库列名字符串,字段重命名时编译期即可发现错误 - 不要在覆写方法中调用
Models.data().queryPage():这会再次路由到函数注册中心,可能形成递归调用;应使用new XxxProxy().queryPage(page, wrapper)直接执行 - 简单过滤优先考虑 DSL filter:如果过滤条件是静态的(如
dataStatus == ENABLED),直接在<node filter="...">中配置 RSQL 即可,无需写代码
Oinone社区 作者:nation原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/25596.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验