發(fā)NC65向?qū)е髯颖韱螕?jù):從零到跑通第一條業(yè)務(wù)鏈)
簡(jiǎn)介這份資源面向用友UAP NC65平臺(tái)的初學(xué)者與二次開(kāi)發(fā)人員聚焦向?qū)介_(kāi)發(fā)主子表單據(jù)這一典型場(chǎng)景幫助讀者理解主表與子表的一對(duì)多關(guān)聯(lián)、數(shù)據(jù)綁定與聯(lián)動(dòng)交互邏輯。包內(nèi)共126個(gè)文件以37個(gè)java源碼與37個(gè)class編譯文件為核心輔以16個(gè)sql建表腳本、6個(gè)properties配置、5個(gè)xml及bmf、bpf、btf等UAP專(zhuān)有元數(shù)據(jù)文件另有pdf說(shuō)明與xlsx表格壓縮包約4.56MB結(jié)構(gòu)完整便于對(duì)照學(xué)習(xí)。目前已有1675人學(xué)習(xí)下載說(shuō)明該主題在企業(yè)級(jí)應(yīng)用開(kāi)發(fā)群體中關(guān)注度較高。讀者可從中獲取可運(yùn)行的示例工程、數(shù)據(jù)庫(kù)腳本與配置模板結(jié)合源碼理解事件處理、數(shù)據(jù)綁定與增刪改查的實(shí)現(xiàn)方式并借助測(cè)試調(diào)試與優(yōu)化思路快速掌握主子表單據(jù)從建表、布局到聯(lián)動(dòng)的完整開(kāi)發(fā)流程為后續(xù)復(fù)雜項(xiàng)目打下基礎(chǔ)。1. 用友UAP開(kāi)發(fā)NC65向?qū)е髯颖韱螕?jù)從零到能跑通的第一條業(yè)務(wù)鏈很多做NC65二次開(kāi)發(fā)的人第一次接觸UAP向?qū)街髯颖韱螕?jù)都會(huì)卡在同一個(gè)地方單據(jù)模板建好了元數(shù)據(jù)也生成了但主子表之間的關(guān)聯(lián)字段死活對(duì)不上保存時(shí)報(bào)“主鍵為空”或者“子表數(shù)據(jù)丟失”。這不是玄學(xué)是向?qū)傻哪J(rèn)配置和實(shí)際業(yè)務(wù)模型之間有斷層。用友UAP開(kāi)發(fā)NC65向?qū)е髯颖韱螕?jù)的詳細(xì)步驟核心要解決的就是三件事主表與子表的物理模型怎么建、向?qū)傻拇a骨架怎么改、單據(jù)模板與元數(shù)據(jù)的綁定關(guān)系怎么校驗(yàn)。適合已經(jīng)了解NC65基本架構(gòu)、能獨(dú)立登錄UAP開(kāi)發(fā)平臺(tái)、手頭有一個(gè)明確主子表業(yè)務(wù)場(chǎng)景比如訂單-明細(xì)、合同-條款的開(kāi)發(fā)者。下面按實(shí)際動(dòng)手順序拆開(kāi)講每一步都給出可復(fù)現(xiàn)的操作和參數(shù)。2. 主子表模型設(shè)計(jì)先定數(shù)據(jù)庫(kù)結(jié)構(gòu)再動(dòng)向?qū)?.1 主表與子表的字段規(guī)劃原則在UAP里建主子表單據(jù)第一步不是打開(kāi)向?qū)Ф前褦?shù)據(jù)庫(kù)表結(jié)構(gòu)想清楚。主表存單據(jù)頭信息子表存行項(xiàng)目信息兩者通過(guò)主表主鍵關(guān)聯(lián)。常見(jiàn)做法是主表用PK_主表名作為主鍵子表用PK_子表名作為自身主鍵同時(shí)用PK_主表名作為外鍵指向主表。字段類(lèi)型上主表主鍵用CHAR(20)子表外鍵同類(lèi)型同長(zhǎng)度否則向?qū)申P(guān)聯(lián)時(shí)會(huì)報(bào)類(lèi)型不匹配。我一般會(huì)先在數(shù)據(jù)庫(kù)里把兩張表建好再進(jìn)UAP做元數(shù)據(jù)。這樣做的好處是向?qū)ёx取表結(jié)構(gòu)時(shí)直接映射不用在向?qū)Ю锸謩?dòng)補(bǔ)字段。建表時(shí)注意主表必須有TS時(shí)間戳字段和DR刪除標(biāo)記字段子表同樣需要這是NC65持久化框架的硬性要求。如果漏了后續(xù)生成VO時(shí)框架會(huì)報(bào)字段缺失。-- 主表示例采購(gòu)訂單頭 CREATE TABLE PO_ORDER_H ( PK_ORDER_H CHAR(20) NOT NULL PRIMARY KEY, ORDER_CODE VARCHAR2(50), SUPPLIER VARCHAR2(100), ORDER_DATE CHAR(19), TS CHAR(19), DR NUMBER(10) DEFAULT 0 ); -- 子表示例采購(gòu)訂單行 CREATE TABLE PO_ORDER_B ( PK_ORDER_B CHAR(20) NOT NULL PRIMARY KEY, PK_ORDER_H CHAR(20), MATERIAL_CODE VARCHAR2(50), QUANTITY NUMBER(18,4), PRICE NUMBER(18,4), TS CHAR(19), DR NUMBER(10) DEFAULT 0 );上面建表語(yǔ)句里PK_ORDER_H在主表和子表都出現(xiàn)主表里是主鍵子表里是外鍵。TS和DR是NC65所有業(yè)務(wù)表的標(biāo)配TS用于樂(lè)觀鎖DR用于邏輯刪除。字段長(zhǎng)度和類(lèi)型一旦定了后面向?qū)Ю锊灰母牧艘匦律稍獢?shù)據(jù)。2.2 在UAP中注冊(cè)數(shù)據(jù)源與元數(shù)據(jù)表建好后登錄UAP開(kāi)發(fā)平臺(tái)進(jìn)入“數(shù)據(jù)建模”模塊。先注冊(cè)數(shù)據(jù)源指向你建表的那個(gè)數(shù)據(jù)庫(kù)實(shí)例。數(shù)據(jù)源配置里數(shù)據(jù)庫(kù)類(lèi)型選Oracle或SQLServer連接URL按實(shí)際填用戶(hù)名密碼用建表那個(gè)schema的。注冊(cè)完數(shù)據(jù)源后新建元數(shù)據(jù)選擇“從數(shù)據(jù)庫(kù)表導(dǎo)入”把剛才建的兩張表勾上。導(dǎo)入時(shí)注意主表和子表要分別導(dǎo)入不要試圖一次導(dǎo)入成主子結(jié)構(gòu)。導(dǎo)入完成后在元數(shù)據(jù)列表里能看到兩張獨(dú)立的實(shí)體。接下來(lái)手動(dòng)建立關(guān)聯(lián)打開(kāi)主表元數(shù)據(jù)在“子實(shí)體”屬性里添加子表關(guān)聯(lián)字段選PK_ORDER_H對(duì)PK_ORDER_H。這一步是向?qū)ёR(shí)別主子表的關(guān)鍵如果這里沒(méi)配后面向?qū)傻木褪莾蓮垎伪?。配完關(guān)聯(lián)后檢查主表元數(shù)據(jù)的“主鍵生成策略”一般選“自定義主鍵”或“序列”不要選“數(shù)據(jù)庫(kù)自增”因?yàn)镹C65的持久化框架需要自己控制主鍵。子表元數(shù)據(jù)里PK_ORDER_H字段要標(biāo)記為“外鍵”這樣向?qū)蒝O時(shí)才會(huì)帶上父子引用。2.3 向?qū)汕暗臋z查清單打開(kāi)向?qū)е按_認(rèn)這幾項(xiàng)主表元數(shù)據(jù)里子實(shí)體已關(guān)聯(lián)、主鍵策略已設(shè)、TS和DR字段已映射子表元數(shù)據(jù)里外鍵已標(biāo)記、主鍵已設(shè)。另外兩張表的字段注釋最好在數(shù)據(jù)庫(kù)里就寫(xiě)好向?qū)?huì)把注釋帶到界面標(biāo)簽上省得后面一個(gè)個(gè)改。還有一個(gè)容易忽略的點(diǎn)元數(shù)據(jù)里的“顯示名”和“字段名”要區(qū)分開(kāi)。顯示名是界面上看到的字段名是數(shù)據(jù)庫(kù)列名。向?qū)山缑鏁r(shí)用顯示名生成VO時(shí)用字段名。如果顯示名里有空格或特殊字符界面可能顯示異常建議顯示名用中文字段名用英文大寫(xiě)加下劃線(xiàn)。3. 向?qū)渲门c代碼生成把默認(rèn)骨架改成能用的3.1 啟動(dòng)向?qū)Р⑦x擇主子表模板在UAP開(kāi)發(fā)平臺(tái)里右鍵點(diǎn)擊項(xiàng)目節(jié)點(diǎn)選擇“新建”-“單據(jù)向?qū)А薄O驅(qū)У谝徊竭x模板類(lèi)型這里要選“主子表單據(jù)”不要選“單表”或“樹(shù)形”。選完后向?qū)?huì)讓你指定主表和子表的元數(shù)據(jù)。主表選PO_ORDER_H子表選PO_ORDER_B關(guān)聯(lián)字段向?qū)?huì)自動(dòng)帶出來(lái)確認(rèn)是PK_ORDER_H即可。下一步是配置界面布局。向?qū)J(rèn)給主表字段放上半部分子表字段放下半部分表格。我一般會(huì)把主表里不需要用戶(hù)輸入的字段比如TS、DR、主鍵隱藏掉只留業(yè)務(wù)字段。子表表格里把外鍵字段也隱藏因?yàn)楸4鏁r(shí)框架會(huì)自動(dòng)填充。這一步在向?qū)У摹白侄慰梢?jiàn)性”里勾選不要生成后再去代碼里改改起來(lái)麻煩。3.2 生成后的代碼結(jié)構(gòu)解析向?qū)瓿珊髸?huì)在項(xiàng)目里生成一組文件主表VO、子表VO、主表DAO、子表DAO、主表Service、子表Service、主表Controller、子表Controller以及界面配置文件。目錄結(jié)構(gòu)一般是src/模塊名/單據(jù)名/下面分vo、dao、service、controller四個(gè)包。主表VO里會(huì)有一個(gè)List子表VO類(lèi)型的屬性名字通常是子表VO類(lèi)名首字母小寫(xiě)加s。這個(gè)屬性是主子表聯(lián)動(dòng)的核心保存時(shí)框架會(huì)遍歷這個(gè)List把子表數(shù)據(jù)一起持久化。如果向?qū)珊筮@個(gè)List沒(méi)出現(xiàn)說(shuō)明元數(shù)據(jù)關(guān)聯(lián)沒(méi)配好要回去檢查。// 主表VO示例向?qū)珊笫謩?dòng)補(bǔ)充注釋 public class OrderHVO { private String pkOrderH; // 主鍵 private String orderCode; // 單據(jù)編號(hào) private String supplier; // 供應(yīng)商 private String orderDate; // 單據(jù)日期 private String ts; // 時(shí)間戳 private Integer dr; // 刪除標(biāo)記 private ListOrderBVO orderBs; // 子表集合向?qū)ё詣?dòng)生成 // getter/setter省略 }上面代碼里orderBs就是子表集合。向?qū)蓵r(shí)如果元數(shù)據(jù)關(guān)聯(lián)正確這個(gè)屬性會(huì)自動(dòng)加上。如果沒(méi)有手動(dòng)加一個(gè)ListOrderBVO屬性名字要和子表DAO里查詢(xún)時(shí)用的名字一致。3.3 主鍵生成與子表外鍵填充的代碼修改向?qū)傻拇a里主鍵生成邏輯默認(rèn)是空的需要手動(dòng)補(bǔ)。我一般會(huì)在主表Service的insert方法里用UUID或序列生成主鍵然后遍歷子表集合把主鍵賦給子表的外鍵字段。這一步不做保存時(shí)子表外鍵為空數(shù)據(jù)庫(kù)里就是臟數(shù)據(jù)。// 主表Service的保存方法片段 public void saveOrder(OrderHVO orderH) { // 生成主鍵 String pk UUID.randomUUID().toString().replace(-, ).substring(0, 20); orderH.setPkOrderH(pk); // 遍歷子表填充外鍵 if (orderH.getOrderBs() ! null) { for (OrderBVO orderB : orderH.getOrderBs()) { orderB.setPkOrderH(pk); // 外鍵指向主表主鍵 orderB.setPkOrderB(UUID.randomUUID().toString().replace(-, ).substring(0, 20)); } } // 調(diào)用DAO保存主表 orderHDao.insert(orderH); // 調(diào)用DAO批量保存子表 if (orderH.getOrderBs() ! null) { for (OrderBVO orderB : orderH.getOrderBs()) { orderBDao.insert(orderB); } } }這段代碼里主鍵用UUID截取20位和數(shù)據(jù)庫(kù)CHAR(20)對(duì)齊。子表外鍵pkOrderH直接賦主表主鍵值子表自身主鍵也生成一個(gè)。注意insert順序先主表后子表因?yàn)樽颖硗怄I依賴(lài)主表主鍵。如果數(shù)據(jù)庫(kù)有外鍵約束順序反了會(huì)報(bào)錯(cuò)。3.4 界面配置與事件綁定向?qū)傻慕缑媾渲梦募镏鞅碜侄魏妥颖肀砀褚呀?jīng)綁定了VO屬性。需要手動(dòng)加的是按鈕事件保存、新增行、刪除行。保存按鈕綁定主表Controller的save方法新增行按鈕綁定子表Controller的addRow方法刪除行綁定deleteRow。這些在界面設(shè)計(jì)器里通過(guò)“事件綁定”面板操作選對(duì)應(yīng)按鈕選事件類(lèi)型填方法名。新增行時(shí)子表Controller里要?jiǎng)?chuàng)建一個(gè)新的子表VO實(shí)例加到主表VO的子表集合里然后刷新表格。刪除行時(shí)從集合里移除選中行再刷新。刷新表格用table.refresh()或重新設(shè)數(shù)據(jù)源。這一步不寫(xiě)界面上點(diǎn)新增沒(méi)反應(yīng)。4. 避坑與排查主子表單據(jù)開(kāi)發(fā)中最容易翻車(chē)的五個(gè)點(diǎn)4.1 保存時(shí)報(bào)“主鍵為空”現(xiàn)象點(diǎn)保存后后臺(tái)日志報(bào)主鍵字段為空數(shù)據(jù)沒(méi)進(jìn)庫(kù)。原因主表Service里沒(méi)生成主鍵或者生成后沒(méi)設(shè)到VO里。解決在保存方法第一行就生成主鍵并set進(jìn)去確保后續(xù)子表外鍵填充用的是同一個(gè)值。如果用了序列檢查序列是否已創(chuàng)建、是否有權(quán)限。4.2 子表數(shù)據(jù)保存后丟失現(xiàn)象主表保存成功子表數(shù)據(jù)庫(kù)里沒(méi)數(shù)據(jù)。原因子表集合為空或者子表DAO的insert沒(méi)被調(diào)用。解決先在保存方法里打印子表集合大小確認(rèn)界面?zhèn)鬟^(guò)來(lái)的數(shù)據(jù)有沒(méi)有進(jìn)集合。如果集合有數(shù)據(jù)但沒(méi)入庫(kù)檢查子表DAO的insert方法是否被正確調(diào)用以及子表外鍵是否已填充。外鍵為空時(shí)有些框架會(huì)靜默跳過(guò)。4.3 界面子表表格不顯示數(shù)據(jù)現(xiàn)象查詢(xún)主表后子表表格空白。原因查詢(xún)方法只查了主表沒(méi)查子表或者查了但沒(méi)設(shè)到主表VO的子表集合里。解決在主表查詢(xún)方法里根據(jù)主表主鍵再查一次子表把結(jié)果set到主表VO的orderBs屬性。查詢(xún)子表時(shí)用外鍵字段過(guò)濾不要用主鍵。4.4 修改時(shí)子表數(shù)據(jù)重復(fù)插入現(xiàn)象修改主表后保存子表數(shù)據(jù)多了一份。原因保存方法里對(duì)子表只做了insert沒(méi)做delete再insert或者沒(méi)判斷是新增還是修改。解決修改場(chǎng)景下先根據(jù)主表主鍵刪除子表舊數(shù)據(jù)再插入新數(shù)據(jù)?;蛘哂胾pdate逐條更新但要注意子表主鍵是否變化。4.5 向?qū)珊笤獢?shù)據(jù)關(guān)聯(lián)丟失現(xiàn)象重新打開(kāi)向?qū)Щ蛑匦律纱a后主子表關(guān)聯(lián)沒(méi)了。原因元數(shù)據(jù)里子實(shí)體關(guān)聯(lián)被覆蓋或者向?qū)Ь彺媪伺f配置。解決每次重新生成前先備份元數(shù)據(jù)配置。如果關(guān)聯(lián)丟了手動(dòng)在元數(shù)據(jù)里重新配子實(shí)體再生成。不要依賴(lài)向?qū)У摹坝涀∩洗闻渲谩蹦莻€(gè)功能在跨會(huì)話(huà)時(shí)不可靠。5. 進(jìn)階技巧用模板化配置減少重復(fù)勞動(dòng)5.1 把常用字段和事件抽成模板做多了會(huì)發(fā)現(xiàn)主子表單據(jù)的很多字段和事件是重復(fù)的單據(jù)編號(hào)、單據(jù)日期、制單人、制單時(shí)間這些幾乎每個(gè)單據(jù)都有。我一般會(huì)在UAP里建一個(gè)“基礎(chǔ)字段模板”把這些字段的元數(shù)據(jù)定義好新建單據(jù)時(shí)直接引用模板不用每次手動(dòng)加。事件綁定也一樣保存、新增行、刪除行的代碼邏輯基本固定可以抽成一個(gè)基類(lèi)子類(lèi)繼承后只改業(yè)務(wù)字段。// 基類(lèi)示例封裝主子表通用保存邏輯 public abstract class BaseOrderService { public void saveOrder(BaseOrderHVO orderH) { String pk generatePk(); orderH.setPkOrderH(pk); fillChildFk(orderH, pk); saveMain(orderH); saveChildren(orderH); } protected abstract void saveMain(BaseOrderHVO orderH); protected abstract void saveChildren(BaseOrderHVO orderH); protected abstract void fillChildFk(BaseOrderHVO orderH, String pk); private String generatePk() { return UUID.randomUUID().toString().replace(-, ).substring(0, 20); } }這個(gè)基類(lèi)把主鍵生成、外鍵填充、保存順序固定下來(lái)子類(lèi)只實(shí)現(xiàn)具體的主表和子表保存邏輯。這樣新單據(jù)開(kāi)發(fā)時(shí)Service層代碼量能減少一半以上。注意基類(lèi)里的BaseOrderHVO要定義子表集合的通用接口比如getChildren()和setChildren()讓子類(lèi)VO實(shí)現(xiàn)。5.2 用校驗(yàn)規(guī)則攔截臟數(shù)據(jù)保存前加校驗(yàn)?zāi)鼙苊夂芏嗪笃谂挪椤N乙话銜?huì)在主表Service的保存方法開(kāi)頭加幾行校驗(yàn)主表必填字段是否為空、子表集合是否為空、子表每行的數(shù)量是否大于零。校驗(yàn)不通過(guò)直接拋異常界面上彈提示。這樣比數(shù)據(jù)進(jìn)庫(kù)后再去數(shù)據(jù)庫(kù)里撈要高效得多。校驗(yàn)規(guī)則可以配在元數(shù)據(jù)里也可以寫(xiě)在代碼里。元數(shù)據(jù)里配的校驗(yàn)向?qū)山缑鏁r(shí)會(huì)自動(dòng)加前端校驗(yàn)但后端還是要再校驗(yàn)一次防止繞過(guò)界面直接調(diào)接口。代碼里的校驗(yàn)用if判斷加throw new BusinessException(提示信息)異常信息會(huì)傳到前端顯示。5.3 查詢(xún)性能優(yōu)化子表分頁(yè)與懶加載主子表單據(jù)查詢(xún)時(shí)如果子表數(shù)據(jù)量大一次性全查出來(lái)會(huì)拖慢界面。我一般會(huì)在子表查詢(xún)里加分頁(yè)默認(rèn)每頁(yè)50條界面上加翻頁(yè)按鈕?;蛘哂脩屑虞d主表查出來(lái)后子表先不查等用戶(hù)點(diǎn)開(kāi)某行主表時(shí)再查對(duì)應(yīng)子表。UAP的表格組件支持懶加載模式配置一下lazyLoad屬性即可。分頁(yè)查詢(xún)的SQL里用ROWNUM或LIMIT控制返回條數(shù)同時(shí)查一個(gè)總數(shù)用于顯示頁(yè)碼。注意分頁(yè)參數(shù)要傳到后端前端表格的onPageChange事件里調(diào)查詢(xún)方法把當(dāng)前頁(yè)碼和每頁(yè)條數(shù)傳過(guò)去。這一步不做子表數(shù)據(jù)超過(guò)幾百條時(shí)界面會(huì)明顯卡頓。5.4 用日志定位保存失敗的具體字段保存失敗時(shí)后臺(tái)日志往往只報(bào)一個(gè)異常堆棧不告訴你哪個(gè)字段有問(wèn)題。我習(xí)慣在保存方法里加一段日志把主表和子表的關(guān)鍵字段值打印出來(lái)尤其是主鍵、外鍵、必填字段。這樣一旦報(bào)錯(cuò)看日志就能定位到具體是哪條數(shù)據(jù)、哪個(gè)字段。// 保存前打印關(guān)鍵字段日志 logger.info(保存主表pk{}, code{}, supplier{}, orderH.getPkOrderH(), orderH.getOrderCode(), orderH.getSupplier()); if (orderH.getOrderBs() ! null) { for (OrderBVO orderB : orderH.getOrderBs()) { logger.info(子表行pk{}, fk{}, material{}, qty{}, orderB.getPkOrderB(), orderB.getPkOrderH(), orderB.getMaterialCode(), orderB.getQuantity()); } }日志級(jí)別用info就行不要用debug因?yàn)樯a(chǎn)環(huán)境一般不開(kāi)debug。打印時(shí)注意不要打印敏感信息比如價(jià)格、客戶(hù)名稱(chēng)可以用掩碼或只打印長(zhǎng)度。這段日志在排查“子表外鍵為空”或“主鍵重復(fù)”時(shí)特別有用比斷點(diǎn)調(diào)試快。5.5 版本兼容不同NC65小版本的向?qū)Р町怤C65有幾個(gè)小版本UAP向?qū)У慕缑婧蜕纱a略有差異。比如某些版本里子表集合的屬性名是children某些版本是子表VO類(lèi)名首字母小寫(xiě)加s。生成代碼后先看一眼屬性名和DAO里查詢(xún)時(shí)用的名字對(duì)不上就手動(dòng)改。另外元數(shù)據(jù)導(dǎo)入時(shí)某些版本對(duì)TS字段的類(lèi)型要求是CHAR(19)某些版本接受TIMESTAMP建表時(shí)統(tǒng)一用CHAR(19)最保險(xiǎn)。如果團(tuán)隊(duì)里有人用不同小版本建議在項(xiàng)目文檔里記下當(dāng)前使用的NC65具體版本號(hào)和UAP補(bǔ)丁號(hào)。遇到向?qū)袨椴灰恢聲r(shí)先核對(duì)版本不要盲目改代碼。我吃過(guò)這個(gè)虧在一個(gè)版本上調(diào)通的配置換到另一個(gè)版本上生成出來(lái)少了一個(gè)關(guān)聯(lián)屬性查了半天以為是元數(shù)據(jù)問(wèn)題最后發(fā)現(xiàn)是版本差異。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取