金蝶云苍穹插件开发:查询、关联与单据体新增实战指南
简介这份《金蝶云苍穹插件操作指南精华版》以 PDF 形式呈现面向正在接触金蝶云苍穹平台插件开发的企业应用开发者与二次开发人员重点解决单个对象查询、结果集使用及关联数据获取等常见问题。内容围绕 BusinessDataServiceHelper、QueryServiceHelper 等核心类库展开说明单条数据、多条数据、缓存读取、存在性判断等方法的差异并结合请假申请单中修改请假类型后查询剩余天数的场景演示查询条件组装、返回结果封装、取值与界面赋值同时延伸到多对象关联查询与最优查询思路帮助读者理解查询条件、返回结果和页面交互之间的衔接。资源仅含 1 个 PDF 文件压缩包约 2.97MB便于集中查阅。已有 759 人学习适合希望快速掌握苍穹插件查询接口、补齐代码实现细节的开发者参考。1. 从一次请假单联动查询说起请假申请单上有个很常见的联动需求用户改了请假类型页面要立刻显示这个人在该类型下还剩多少天。看起来是一次简单赋值实际涉及三个技术决策——用哪个 ServiceHelper 查、查回来的对象怎么取字段、关联字段的引用属性有没有勾。苍穹的二开插件里绝大多数查不到数据取属性报错关联字段返回的是 ID 不是对象的坑根源都在这三点上。这份《金蝶云苍穹插件操作指南精华版》拆的正是这件事从单对象查询、多对象关联查询到单据新增含单据体、子单据体覆盖了表单插件和单据插件里最高频的数据操作场景。它不适合当作 API 手册翻而适合在写AbstractFormPlugin或AbstractBillPlugIn时对照着看——因为里面把BusinessDataServiceHelper和QueryServiceHelper的行为差异讲得比官方 API 注释清楚。下面按查询 → 关联 → 新增的顺序把可复现的部分全部落地。2. QueryServiceHelper 与 BusinessDataServiceHelper 的单据查询差异选错 Helper 是第一个分水岭。两者都能查单条、多条但在返回什么这件事上完全不同如果只记住都能查就去写代码后面必然返工。2.1 两个 Helper 的核心行为对比先把结论摆出来写代码前扫一眼这张表能省掉大量调试时间对比项QueryServiceHelperBusinessDataServiceHelper关联字段返回值主键 IDLongDynamicObject 对象关联对象默认属性无必须显式指定id/number/name/multilanguagetext/masterid返回数据是否含 pkId不含含第一级*查询支持不支持支持缓存方法无loadSingleFromCache / loadFromCacheQueryServiceHelper更接近我只要这几列的 SQL 思维性能上更可控BusinessDataServiceHelper更接近给我一个可用对象的领域思维适合拿到单据后还要继续往关联对象上取值的场景。我个人在纯读值展示时优先用前者在需要拿到对象继续操作时用后者。2.2 单对象查询的完整实现以请假类型变更后查剩余天数为例子代码结构是监听propertyChanged组装QFilter查询赋值。public class GetSingleData extends AbstractFormPlugin { private final static String KEY_LEAVE_TYPE leavetype; private final static String KEY_PERSON person; private final static String KEY_LEAVE_DAYS_BASE_DATA 9z8e_gjt_hr_qjts; Override public void propertyChanged(PropertyChangedArgs e) { super.propertyChanged(e); // 只在请假类型变更时触发避免其他字段变更带来无谓查询 if (StringUtils.equals(KEY_LEAVE_TYPE, e.getProperty().getName())) { IDataModel model this.getModel(); DynamicObject personObj (DynamicObject) model.getValue(KEY_PERSON); DynamicObject leaveTypeObj (DynamicObject) model.getValue(KEY_LEAVE_TYPE); if (null ! personObj null ! leaveTypeObj) { Long personID (Long) personObj.getPkValue(); Long leaveTypeID (Long) leaveTypeObj.getPkValue(); // 两个条件用 and 组合避免查错记录 QFilter personQFilter new QFilter(KEY_PERSON, QCP.equals, personID); QFilter leavetypeFilter new QFilter(KEY_LEAVE_TYPE, QCP.equals, leaveTypeID); QFilter[] qFilters { personQFilter.and(leavetypeFilter) }; // 只取需要的两列减少 IO DynamicObject leavedaysObj QueryServiceHelper.queryOne( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays, qFilters); if (leavedaysObj ! null) { Long unusedays leavedaysObj.getLong(unusedays); model.setValue(unusedays, unusedays); } } } } }参数上要注意三点。第一queryOne的三个参数依次是实体标识、select 字段串、过滤条件数组字段串是逗号分隔的字符串而不是数组写的时候别用List。第二model.getValue()对于基础资料控件拿到的是DynamicObject要取主键必须走getPkValue()而不是getId()——后者在某些场景拿不到。第三propertyChanged在界面初始化时不触发也就是说afterCreateNewData里改字段值不会引发这个事件需要初始赋值的话得单独在afterBindData里补一段。2.3 queryOne 与 loadSingle 的等价替换同样的查询换成BusinessDataServiceHelper.loadSingle()签名一致、语义一致返回的DynamicObject还带了 pkIdDynamicObject leavedaysObj2 BusinessDataServiceHelper.loadSingle( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays, qFilters); if (leavedaysObj2 ! null) { Long unusedays leavedaysObj2.getLong(unusedays); model.setValue(unusedays, unusedays); }什么时候用它如果你后面还要拿这个基础资料的名称去做提示、或者要判断它是否存在loadSingle更省事——name是默认返回的。反之如果只是取两个数值做计算queryOne的返回体更薄走网络传输和反序列化的开销更小。高频触发的propertyChanged里我一般用queryOne。3. 关联字段查询引用属性勾选与*的层级规则单据几乎不可能孤立存在请假单关联请假人请假人又关联部门和职位。关联查询最容易踩的坑不是写法而是字段查不出来还报错原因基本都指向引用属性。3.1 关联字段的三种返回形态以前面的单对象查询为基础把 select 字段串改成带person.前缀的写法能直观看到差异// 方式一QueryServiceHelper必须显式列出关联字段的属性 DynamicObject obj QueryServiceHelper.queryOne( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays,person,person.name,person.phone, qFilters); Object person obj.get(person); // 返回的是主键 IDLong Object name obj.get(person.name); // 显式指定后才能取到 Object phone obj.get(person.phone); // 方式二BusinessDataServiceHelper关联字段默认带 5 个属性 DynamicObject obj2 BusinessDataServiceHelper.loadSingle( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays,person,person.phone, qFilters); Object person2 obj2.get(person); // 返回 DynamicObject DynamicObject personObj (DynamicObject) person2; String name2 personObj.getString(name); // 默认属性可直接取提示BusinessDataServiceHelper的关联字段默认只返回 id、number、name、multilanguagetext、masterid 五个属性。想取其他属性必须先到单据的关联控件里在引用属性中勾上该字段否则查询时会直接抛没有该属性的异常。这个引用属性勾选的动作很多人以为只是界面配置实际上它直接决定了查询时的 SQL 会 join 出哪些列。换句话说引用属性就是你的 select 白名单。3.2*查询的层级边界想一次把所有属性都捞出来*是最省事的写法但它有明确的边界三种查询方式各不相同// 1. BusinessDataServiceHelper第一级 * 无效第二级 * 有效 DynamicObject a BusinessDataServiceHelper.loadSingle( KEY_LEAVE_DAYS_BASE_DATA, *,person.*, qFilters); // 2. QueryServiceHelper支持 *但依赖引用属性勾选 DynamicObject b QueryServiceHelper.queryOne( KEY_LEAVE_DAYS_BASE_DATA, *,person.*, qFilters); // 3. ORM不传字段参数即查全部默认穿透 3 层 DynamicObject c ORM.create().queryOne(KEY_LEAVE_DAYS_BASE_DATA, qFilters); // 也可以显式指定同样支持 * DynamicObject d ORM.create().queryOne( KEY_LEAVE_DAYS_BASE_DATA, *,person.*, qFilters);ORM 的默认穿透层数是个容易被忽略的点它默认查到关联表的关联表为止也就是三层。如果你的单据模型是四层关联第四层不会自动带出来必须显式指定。另外无论哪种方式关联表能返回哪些属性最终还是由引用属性说了算——*只是在可返回范围内全要不是绕过配置全要。3.3 按关联深度选择查询策略实际开发里关联查询可以按需求切成几档选择依据是要不要关联表的关联表只要本单字段直接*或列出字段不碰关联。要关联表的部分字段显式写person.name,person.phoneQueryServiceHelper 必须列全。要关联表的全部字段person.*前提是引用属性勾满。要关联表的关联表ORM 默认三层更深层级要显式指定并勾对应引用属性。如果只是想在页面上展示一下关联对象的名称用person.name就够如果要把关联对象整个拿去做后续业务逻辑计算loadSingle返回的 DynamicObject 更顺手因为它带有主键可以直接再发起下一轮查询。4. 单据体与子单据体查询entryentity 的取数逻辑主从结构的查询比单头复杂一档因为返回的行数不再是一行。单据体默认标识entryentity子单据体默认subentryentity这两个约定值贯穿所有查询方式。4.1 三种查询方式对单据体的支持度同样是*和entryentity.*三种 Helper 的表现可以列成一张对照表查询方式写法结果BusinessDataServiceHelper*不返回单据体BusinessDataServiceHelperentryentity.textfield返回对应属性和分录 idQueryServiceHelper*不返回单据体QueryServiceHelperentryentity.*只返回单据体第一条QueryServiceHelper*,entryentity.*返回全部单据体数据ORM不传字段返回全部含单据体和子单据体ORM*不返回单据体ORMid,entryentity.*返回单据体数据从表里能看出QueryServiceHelper和ORM想拿全单据体都得显式把entryentity.*写进字段串并且 ORM 还要额外带上单据 id。BusinessDataServiceHelper则干脆不支持单据体的*只能按字段逐个列。4.2 主从查询与主从从查询代码单头加全部单据体数据用QueryServiceHelper.query()返回DynamicObjectCollectionDynamicObjectCollection rows QueryServiceHelper.query( KEY_LEAVE_APPLY, *,entryentity.*, filters); // 每行是单据头 一条单据体数据的扁平结构 for (DynamicObject row : rows) { String billno row.getString(billno); String text row.getString(entryentity.textfield); Long entryId row.getLong(entryentity.id); // 分录 id 也一并返回 }再往下到子单据体主从从写法是entryentity.subentryentity.*注意BusinessDataServiceHelper不支持子单据体只能用QueryServiceHelper或者 ORM// QueryServiceHelper加 entryentity.* 才能同时拿到单据体信息 DynamicObjectCollection rows2 QueryServiceHelper.query( KEY_LEAVE_APPLY, entryentity.subentryentity.*, filters); // ORM指定查询时必须带上单据 id 和单据体 id否则子单据体取不出来 DynamicObject bill ORM.create().queryOne( KEY_LEAVE_APPLY, id,entryentity.id,entryentity.subentryentity.*, filters);注意ORM 指定字段查询子单据体时id和entryentity.id是不能省的。少了单据体 id引擎无法把子单据体记录和父分录关联起来返回结果里子单据体就是空的而且不会报错——这种静默失败最难排查。返回多条数据时统一用DynamicObjectCollection接收遍历可以用 foreach数据量大的话用Iterator更省内存。需要只取一条时换成queryOne()语义上就是limit 1。5. 插件里新增单据与单据体的落库细节查询是读新增是写。新增走的是SaveServiceHelper核心方法就两个save()和saveOperate()区别决定了你的单据能不能正常被后续流程处理。5.1 save 与 saveOperate 的选择DynamicObject log BusinessDataServiceHelper.newDynamicObject(KEY_LOG_PAGE); log.set(KEY_PERSON, this.getModel().getValue(KEY_PERSON)); log.set(KEY_ORG, this.getModel().getValue(KEY_ORG)); log.set(billpkid, this.getModel().getDataEntity().getPkValue()); log.set(billcode, this.getView().getEntityId()); log.set(operationcode, evt.getItemKey()); log.set(operationkey, evt.getOperationKey()); log.set(operationdate, new Date()); log.set(billstatus, A); // 暂存状态不设会影响后续操作 // 方式一直接插库不走编码规则等外围逻辑 // SaveServiceHelper.save(new DynamicObject[]{ log }); // 方式二走新增操作链路会补全编码规则等数据 SaveServiceHelper.saveOperate(KEY_LOG_PAGE, new DynamicObject[]{ log }, null);参数上newDynamicObject(实体标识)只是构造一个内存对象此时还没有主键save()直接落库速度最快但不会触发编码规则、不会补全一些平台自动维护的字段saveOperate()的第二个参数是待保存对象数组第三个参数是可选的操作参数一般传null它会走框架的新增操作链。绝大多数业务场景应该用saveOperate()除非你明确知道自己在做批量导入这类性能敏感的动作。billstatus这个字段值得单独说设置为A暂存后单据才能在后续被提交、审核等操作正常流转。不设置状态值时单据虽然能落库但走后续操作时可能因为状态为空而报错。5.2 单据体数据的构造方式单据体不是单独保存的而是挂到主对象上再一起落库。取单据体对象后调addNew()创建行DynamicObjectCollection entries (DynamicObjectCollection) log.get(entryentity); // 第一行 DynamicObject row1 entries.addNew(); row1.set(date, new Date()); row1.set(text, 分录 1); // 第二行 DynamicObject row2 entries.addNew(); row2.set(date, new Date()); row2.set(text, 分录 2); // 最后和主对象一起提交 SaveServiceHelper.saveOperate(KEY_LOG_PAGE, new DynamicObject[]{ log }, null);addNew()返回的是一个已经和父集合关联好的 DynamicObject对它set的值会自动计入父对象。哪怕只是空分录也必须先addNew()再set直接set单据体字段是不生效的。子单据体同理先拿到分录对象再get(subentryentity)转成集合继续addNew()。保存时只需要保存最外层的主对象框架会自动按主子关系批量写库。6. 查询性能压榨与两类静默报错的排查前面把功能跑通了但插件一旦放到列表批量场景或者高频事件里性能和异常处理就成了分水岭。下面两个技巧是我在自己的单据插件里长期使用的。6.1 缓存接口的适用边界BusinessDataServiceHelper提供了loadSingleFromCache()和loadFromCache()命中时直接走平台缓存不走数据库。基础资料这类变更频率低的数据非常适合// 基础资料查询走缓存减少数据库压力 DynamicObject base BusinessDataServiceHelper.loadSingleFromCache( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays, qFilters);但缓存不是万能药。请假剩余天数是会随审批实时变动的业务数据用它就要接受读到的是缓存快照这一事实。我一般的判断标准是数据变更由用户审批流程驱动、且变更后要求立即可见的不走缓存组织架构、币别、计量单位这类配置型基础资料走缓存。QueryServiceHelper没有缓存方法这也是它在高频只读场景下要先评估的地方。6.2 关联属性报错与空结果的定位顺序遇到问题不要急着重写代码按这个顺序过一遍八成能定位现象排查点处理取person.phone报无此属性关联控件引用属性未勾选在单据设计器勾上 phone查询正常但关联字段为 nullQueryServiceHelper未显式指定该字段字段串补上person.name关联字段拿到 Long 不是对象用错 Helper换loadSingle或转 ID 再查单据体返回 0 行字段串只写了*补entryentity.*子单据体为空且不报错ORM 少写entryentity.id补上单据体和单据 id验证时最省事的做法是把结果对象直接toString()打到前端消息里先确认拿到的是不是你想要的结构再去写取值逻辑。this.getView().showMessage(obj )这一行在调试期比断点还快因为插件运行在服务端日志和前端消息是你能最快看到的反馈。另外QFilter的and组合方式是链式返回新对象personQFilter.and(leavetypeFilter)之后原始对象没变必须用返回值。多条件时建议按业务顺序逐个拼接避免嵌套时括号层级出错。查询条件里用QCP.equals表示等值范围查询换QCP.greaterThan之类的枚举不要自己拼字符串——拼字符串既容易被注入也容易在类型转换上翻车。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →