资讯详情 金蝶云苍穹插件开发实战:查询、关联与新增避坑指南
📅 2026/10/21 5:48:51
简介这份《金蝶云苍穹插件操作指南精华版》面向金蝶云苍穹平台的企业应用开发者与二次开发人员聚焦插件开发中单个对象查询与结果集使用的核心技能。内容围绕BusinessDataServiceHelper、QueryServiceHelper两大核心类库展开讲解loadSingle、queryOne、queryDataSet等方法的适用场景与差异并结合请假申请单中根据请假类型与请假人查询剩余天数的完整代码示例演示DynamicObject结果封装与QFilter条件组装的实际用法帮助开发者掌握从页面取值到查询赋值的完整链路。资源为1个PDF文件压缩包约2.97MB结构紧凑、便于随查随用。目前已有761人学习适合需要快速上手苍穹插件查询逻辑、提升开发效率的初中级开发者参考。1. 从一次请假单翻车说起这份插件指南到底能解决什么去年帮一个团队排查请假申请单的线上问题现象很典型用户改了请假类型剩余天数死活不刷新日志里一行报错都没有。翻代码发现开发同学在propertyChanged里用BusinessDataServiceHelper.loadSingle查基础资料字段只写了unusedays但页面上还引用了person.name结果取出来是 null赋值逻辑直接静默跳过。这类问题在苍穹插件开发里太常见了——不是 API 不会用而是不知道QueryServiceHelper和BusinessDataServiceHelper在关联字段、主键返回、单据体处理上的边界差异。这份《金蝶云苍穹插件操作指南精华版》PDF 一共 24 页核心就讲三件事单个对象查询与结果集使用、多对象关联查询、单据新增含单据体、子单据体。它适合两类人一是刚接触苍穹插件、被DynamicObject和QFilter绕晕的开发者二是已经能写插件、但查询性能或关联字段取值总出玄学问题的熟手。下面我按“查询 → 关联 → 新增 → 避坑 → 进阶”的顺序把这份资料拆成能直接抄作业的实操笔记。2. 单对象查询QueryServiceHelper 与 BusinessDataServiceHelper 的选型分水岭2.1 两个 Helper 的本质差异资料里反复强调一个点QueryServiceHelper.queryOne()只返回你指定的属性关联字段必须显式写person.name这种路径而BusinessDataServiceHelper.loadSingle()对关联字段有“默认返回五个属性”的机制——id、number、name、multilanguagetext、masterid。这个差异直接决定了你取关联字段时会不会拿到 null。更关键的是主键QueryServiceHelper返回的DynamicObject没有 pkIdBusinessDataServiceHelper有。如果你后续要用getPkValue()做更新或关联选错 Helper 就是血泪经验。我一般这样判断只读展示、字段明确、追求轻量用QueryServiceHelper需要主键、需要关联对象默认属性、后续要参与保存用BusinessDataServiceHelper。2.2 可复现的查询代码与参数拆解下面这段代码来自资料 1.3 节我补了参数说明和调用时机注释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(); // QFilter 支持 and 链式组合注意 QCP.equals 是枚举 QFilter personQFilter new QFilter(KEY_PERSON, QCP.equals, personID); QFilter leavetypeFilter new QFilter(KEY_LEAVE_TYPE, QCP.equals, leaveTypeID); QFilter[] qFilters { personQFilter.and(leavetypeFilter) }; // 方式一QueryServiceHelper只返回 unusedays 和 usedays DynamicObject leavedaysObj QueryServiceHelper.queryOne( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays, qFilters); Long unusedays leavedaysObj.getLong(unusedays); // 方式二BusinessDataServiceHelper同样字段但返回带 pkId DynamicObject leavedaysObj2 BusinessDataServiceHelper.loadSingle( KEY_LEAVE_DAYS_BASE_DATA, unusedays,usedays, qFilters); unusedays leavedaysObj2.getLong(unusedays); model.setValue(unusedays, unusedays); } } } }逻辑说明propertyChanged在界面数据初始化时不触发所以afterCreateNewData里改字段不会进这里这是资料里特别标注的坑。参数上queryOne的第三个参数是QFilter[]多个条件用and拼接loadSingle参数一致但返回对象带主键。如果你把unusedays写成unuseday不会报错只会返回 null然后getLong抛空指针——这就是典型的静默翻车。2.3 缓存方法的适用边界资料提到loadSingleFromCache()和loadFromCache()但没展开。我的经验是基础资料这类读多写少、对实时性要求不高的数据走缓存能明显降低数据库压力但请假剩余天数这种会随审批变动的数据用缓存就要评估脏读风险。常见做法是配置类基础资料用缓存业务单据实时查。如果你不确定先不用缓存等压测出现瓶颈再切。3. 多对象关联查询从单据头到子单据体的字段路径规则3.1 关联字段的三种查询深度资料 2.3 节把关联查询拆成“不查关联、查部分关联、查全部关联”三类核心结论我整理成表查询方式关联字段写法返回内容主键QueryServiceHelper 指定字段person仅关联对象主键 id无QueryServiceHelper 指定路径person.name,person.phone指定属性无BusinessDataServiceHelper 指定字段person默认五属性id/number/name/multilanguagetext/masterid有BusinessDataServiceHelper 指定路径person.phone需先在引用属性勾选否则报错有ORM 不传字段无默认查 3 层所有属性有这里最容易踩的是BusinessDataServiceHelper查person.phone时如果页面关联控件的“引用属性”没勾选 phone运行时报“没有该属性”。资料 2.3.2.3 末尾专门放了截图提醒我把它翻译成操作步骤打开单据设计器 → 找到人员关联字段 → 引用属性 → 勾选需要返回的字段 → 保存。3.2 单据体与子单据体的查询写法单据体默认标识entryentity子单据体默认subentryentity。资料 2.4 节给了一组非常实用的对照// 不查单据体QueryServiceHelper 用 * 不会带出单据体 DynamicObject leaveApply QueryServiceHelper.queryOne(KEY_LEAVE_APPLY, *, filters); // 查单据体第一条entryentity.* 只返回第一条分录 DynamicObject leaveApply QueryServiceHelper.queryOne(KEY_LEAVE_APPLY, entryentity.*, filters); // 查所有单据体用 query 返回 DynamicObjectCollection DynamicObjectCollection leaveApply QueryServiceHelper.query( KEY_LEAVE_APPLY, *,entryentity.*, filters); // 查子单据体entryentity.subentryentity.* DynamicObjectCollection leaveApply QueryServiceHelper.query( KEY_LEAVE_APPLY, entryentity.subentryentity.*, filters); // ORM 默认查所有包括单据体和子单据体 DynamicObject leaveApply ORM.create().queryOne(KEY_LEAVE_APPLY, filters); // ORM 指定子单据体必须带单据 id 和单据体 id DynamicObject leaveApply ORM.create().queryOne( KEY_LEAVE_APPLY, id,entryentity.id,entryentity.subentryentity.*, filters);参数说明queryOne返回单条query返回集合entryentity单独写会报错必须带.属性或.*BusinessDataServiceHelper不支持子单据体查询这是硬边界。我一般查主从从数据时直接用 ORM 不传字段省去拼路径的麻烦但要注意默认只查 3 层再深就查不到。3.3 关联查询的性能取舍资料 2.1 节提了一串问题要不要查关联、查部分还是全部、关联还有关联怎么办。我的实操判断是列表页展示用QueryServiceHelper指定字段避免带出大对象编辑页需要主键和默认属性用BusinessDataServiceHelper后台批处理、数据同步用 ORM 默认查询代码短但要注意内存。如果关联层级超过 3 层建议拆成多次查询在内存组装别硬拼一条 SQL 式的路径。4. 单据新增SaveServiceHelper 的 save 与 saveOperate 怎么选4.1 新增单据头的完整流程资料 3.1 节的核心是SaveServiceHelper两个方法save()直接插库不走编码规则saveOperate()会调用新增相关接口生成编码规则等数据。我一般业务单据用saveOperate日志类、临时记录用save。下面代码来自资料 3.1.1.4我补了状态值说明public class AddNewBillPlugin extends AbstractBillPlugIn { private static final String KEY_LOG_PAGE 9z8e_leave_add_log; private static final String KEY_TOOLBAR tbmain; private static final String KEY_PERSON person; private static final String KEY_ORG org; Override public void registerListener(EventObject e) { super.registerListener(e); addItemClickListeners(KEY_TOOLBAR); } Override public void itemClick(ItemClickEvent evt) { super.itemClick(evt); // 根据页面标识构建动态对象此时还未插库 DynamicObject leaveAddLog BusinessDataServiceHelper.newDynamicObject(KEY_LOG_PAGE); leaveAddLog.set(KEY_PERSON, this.getModel().getValue(KEY_PERSON)); leaveAddLog.set(KEY_ORG, this.getModel().getValue(KEY_ORG)); leaveAddLog.set(billpkid, this.getModel().getDataEntity().getPkValue()); leaveAddLog.set(billcode, this.getView().getEntityId()); leaveAddLog.set(operationcode, evt.getItemKey()); leaveAddLog.set(operationkey, evt.getOperationKey()); leaveAddLog.set(operationdate, new Date()); // 状态值必须设A暂存B已提交C已审核 leaveAddLog.set(billstatus, A); // 直接插库不生成编码规则 // SaveServiceHelper.save(new DynamicObject[]{leaveAddLog}); // 走新增操作会完善编码规则等数据 SaveServiceHelper.saveOperate(KEY_LOG_PAGE, new DynamicObject[]{leaveAddLog}, null); } }参数说明saveOperate第一个参数是页面标识第二个是动态对象数组第三个是操作可选参数一般传 null。状态值不设的话后续提交、审核操作会找不到单据这是资料里用括号强调的注意点。4.2 新增单据体与子单据体单据体数据通过leaveAddLog.get(entryentity)拿到DynamicObjectCollection再addNew()创建分录DynamicObjectCollection entrys (DynamicObjectCollection) leaveAddLog.get(entryentity); DynamicObject entry1 entrys.addNew(); entry1.set(date, new Date()); entry1.set(text, 分录 1); DynamicObject entry2 entrys.addNew(); entry2.set(date, new Date()); entry2.set(text, 分录 2);子单据体同理先拿到单据体分录再entry.get(subentryentity)继续addNew()。资料 3.3 节被截断了但按这个模式推即可。注意addNew()之后必须set关键字段否则插库可能报非空约束。4.3 新增操作的常见误用有人图省事用save插业务单据结果编码规则没生成单号为空后续流程全乱。还有人newDynamicObject之后直接save忘了设billstatus单据在列表里查不到。我的习惯是新增前先确认页面标识、状态值、必填字段三样再决定用哪个 save 方法。5. 避坑与排查五个真实翻车现场现象一改了请假类型剩余天数不刷新。原因propertyChanged里取person或leavetype为 null赋值逻辑被跳过。 解决在if (null ! personObj null ! leaveTypeObj)里加日志确认页面初始化时这两个字段是否已赋值如果是初始化场景改用afterCreateNewData手动触发一次查询。现象二BusinessDataServiceHelper.loadSingle取person.phone报“没有该属性”。原因页面关联控件的引用属性没勾选 phone。 解决单据设计器 → 人员字段 → 引用属性 → 勾选 phone → 保存并重新打开页面。现象三QueryServiceHelper.queryOne返回的对象getPkValue()为 null。原因QueryServiceHelper不返回主键。 解决需要主键就换BusinessDataServiceHelper.loadSingle或者在查询字段里显式加id。现象四查单据体用entryentity单独写直接报错。原因单据体标识不能裸写必须带.属性或.*。 解决查第一条用entryentity.*查全部用query方法配*,entryentity.*。现象五ORM 默认查询查不到第四层关联数据。原因ORM 默认只查 3 层。 解决拆成多次查询或者显式指定更深路径但要注意引用属性勾选和性能。6. 进阶技巧用引用属性勾选控制返回字段减少无效查询资料里反复出现“引用属性勾选”这个动作但很多人只把它当配置步骤没意识到它其实是查询字段的白名单。我的做法是在单据设计阶段就把关联字段的引用属性按场景分三档——最小集只勾 id 和 name、展示集勾列表页要显示的字段、全量集后台同步用。这样写插件时BusinessDataServiceHelper的默认五属性和person.xxx路径取值都有保障不会运行时才发现字段没勾。验证方法很简单写完查询代码后在afterBindData里把DynamicObject的getDataEntityType().getProperties()打出来看实际返回了哪些属性。如果发现少了回去检查引用属性。我一般还会在开发环境开 SQL 日志确认查询有没有带出不必要的关联表。另一个技巧是QFilter的复用。多个查询条件结构相似时把QFilter数组抽成方法避免每次手拼and出错。比如private QFilter[] buildLeaveFilter(Long personId, Long leaveTypeId) { QFilter f1 new QFilter(person, QCP.equals, personId); QFilter f2 new QFilter(leavetype, QCP.equals, leaveTypeId); return new QFilter[]{ f1.and(f2) }; }从那以后我每次写查询插件都强制先确认三件事用哪个 Helper、关联字段勾了没、要不要主键。这三步走完基本不会再出现静默 null 的玄学问题。希望帮到你。本文还有配套的精品资源点击获取