用存儲過程返回游標實例:TaoToken 統(tǒng)一 Key 下的配置骨架與驗證)
1. 為什么存儲過程返回游標在 MyBatis 里總踩坑MyBatis 調(diào)用存儲過程返回游標是很多做企業(yè)級報表、對賬、批量校驗的同學(xué)繞不開的場景。存儲過程里open v_Cursor for select ...把結(jié)果集通過OUT參數(shù)吐回來Java 側(cè)拿到的其實是一個 JDBC 游標對象而不是普通的List。如果你按普通select的寫法去接大概率會拿到一個空集合或者直接拋ORA-01000、Invalid column type之類的異常。這篇聚焦一個真實可跑的實例Oracle 存儲過程Fsp_Plan_CheckPrj接收兩個入?yún)?、返回一個sys_refcursorMyBatis 的 mapper XML 用statementTypeCALLABLE聲明OUT參數(shù)用jdbcTypeCURSOR加resultMap映射列別名。同時我會把 MySQL 場景下沒有原生游標、需要用OUT結(jié)果集或臨時表替代的差異也講清楚避免你從 Oracle 遷到 MySQL 時直接照抄報錯。適合誰看已經(jīng)會寫基礎(chǔ) MyBatis CRUD、但第一次接觸CALLABLE和游標映射的后端同學(xué)或者手上有個老項目存儲過程是 DBA 寫好的你只負責(zé)在 Java 層把它調(diào)通。整篇的配置骨架我會用 TaoToken 統(tǒng)一 Key 來管理模型側(cè)和編碼側(cè)的調(diào)用憑證這樣你在調(diào)試 SQL 映射的同時也能順手把 AI 輔助編碼的入口配好不用在多個平臺之間來回切 Key。核心檢索詞先擺出來MyBatis 調(diào)用存儲過程返回游標關(guān)鍵三件套是statementTypeCALLABLE、modeOUT jdbcTypeCURSOR、resultMap列映射。記住這三個后面所有報錯基本都能定位到其中之一。2. TaoToken 前置統(tǒng)一 Key 與配置骨架在動手寫 mapper 之前先把調(diào)用憑證這塊理順。TaoToken 的作用是把模型對話、編碼計劃、API Key 管理收斂到一個入口你只需要維護一份 Key就能在 IDE 插件、命令行工具、腳本里復(fù)用。官網(wǎng)入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意 API 地址不帶 UTM 參數(shù)配置里填這個就行。先拿 Key進入控制臺 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 頁面創(chuàng)建一個新 Key復(fù)制出來。這個 Key 就是后面所有配置里的apiKey字段。如果你用的是 Claude Code 這類編碼 Agent可以直接走 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它會把長期編碼任務(wù)的額度單獨管理不會和你臨時調(diào)試模型對話的消耗混在一起。配置骨架分兩種格式按你用的工具選。VS Code 系插件一般讀settings.json命令行工具或 Rust 系工具讀config.toml。下面兩份都是可復(fù)制的骨架把sk-你的Key替換成剛才創(chuàng)建的值即可。settings.json骨架{ taotoken.apiKey: sk-你的Key, taotoken.baseUrl: https://taotoken.net/api, taotoken.model: claude-sonnet, taotoken.timeout: 60000, taotoken.maxTokens: 8192 }config.toml骨架[taotoken] api_key sk-你的Key base_url https://taotoken.net/api model claude-sonnet timeout 60000 max_tokens 8192注意base_url只寫到/api不要在后面拼/v1或/chat/completions具體路徑由客戶端自己補。填錯這一層最常見的表現(xiàn)是 404而不是鑒權(quán)失敗排查時先看狀態(tài)碼。Key 創(chuàng)建頁在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文檔在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。文檔里有各語言 SDK 的調(diào)用示例遇到參數(shù)名對不上時以文檔為準。模型對話調(diào)試入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 你可以先在網(wǎng)頁里發(fā)一條消息確認 Key 有效再去配本地工具這樣能把「Key 問題」和「工具配置問題」分開。3. 可復(fù)制配置mapper XML 與 Java 調(diào)用3.1 Oracle 存儲過程與 mapper XML先看存儲過程本身它接收v_grantno、v_deptcode兩個入?yún)⒌谌齻€是OUT游標create or replace procedure Fsp_Plan_CheckPrj( v_grantno varchar2, v_deptcode number, v_cursor out sys_refcursor ) is begin open v_cursor for select s.plan_code, s.plan_dept, s.plan_amount, s.exec_amount, p.cname as plan_name, d.cname as dept_name from Snap_plan_check s left join v_plan p on s.plan_code p.plan_code left join org_office d on s.plan_dept d.off_org_code group by s.plan_code, s.plan_dept, s.plan_amount, s.exec_amount, p.cname, d.cname; end Fsp_Plan_CheckPrj;mapper XML 的關(guān)鍵在于resultMap把列別名映射到 Java 屬性select標簽聲明statementTypeCALLABLEOUT參數(shù)寫modeOUT jdbcTypeCURSOR resultMapcursorMapresultMap typejava.util.HashMap idcursorMap result columnplan_code propertyplan_code/ result columnplan_dept propertyplan_dept/ result columnplan_amount propertyplan_amount/ result columnexec_amount propertyexec_amount/ result columnplan_name propertyplan_name/ result columndept_name propertydept_name/ /resultMap select idcall_Fsp_Plan_CheckPrj parameterTypemap statementTypeCALLABLE {call Fsp_Plan_CheckPrj( #{grantNo, jdbcTypeVARCHAR, modeIN}, #{offOrgCode, jdbcTypeINTEGER, modeIN}, #{v_cursor, modeOUT, jdbcTypeCURSOR, resultMapcursorMap} )} /select這里有幾個容易寫錯的點。jdbcTypeCURSOR是 Oracle 驅(qū)動識別的類型MySQL 沒有這個類型后面會單獨說。resultMap的type用java.util.HashMap是為了讓列名直接作為 key如果你有實體類換成實體類全限定名也行但屬性名要和property對上。modeIN的兩個參數(shù)必須顯式寫jdbcType否則某些驅(qū)動版本會報Invalid column type。3.2 Java 調(diào)用代碼Java 側(cè)把OUT參數(shù)先塞一個空ArrayList占位調(diào)用后 MyBatis 會把游標結(jié)果填回來MapString, Object params new HashMap(); GrantSetting gs this.grantSettingDao.get(grantCode); params.put(grantNo, StringUtils.substring(gs.getGrantNo(), 0, 2)); params.put(offOrgCode, SecurityUtils.getPersonOffOrgCode()); params.put(v_cursor, new ArrayListMapString, Object()); this.batisDao.getSearchList(call_Fsp_Plan_CheckPrj, params); ListMapString, Object rows (ListMapString, Object) params.get(v_cursor); for (MapString, Object row : rows) { System.out.println(row.get(plan_code) / row.get(plan_name)); }getSearchList是你 DAO 里封裝的方法內(nèi)部走sqlSession.selectList(call_Fsp_Plan_CheckPrj, params)。調(diào)用完成后params.get(v_cursor)就是可遍歷的List每個元素是一行key 是resultMap里的property名。實測下來這個 List 是懶加載的如果你在sqlSession關(guān)閉后才去遍歷可能拿不到數(shù)據(jù)所以遍歷動作要放在同一個會話內(nèi)。3.3 MySQL 場景的差異MySQL 存儲過程沒有sys_refcursor這種原生游標類型通常用兩種替代一是存儲過程直接select結(jié)果集MyBatis 用普通select接二是用OUT參數(shù)返回結(jié)果集但驅(qū)動支持有限。如果你從 Oracle 遷過來最穩(wěn)的做法是把OUT游標改成存儲過程內(nèi)的selectmapper 里去掉CALLABLE按普通查詢寫。下面是一個 MySQL 的等價寫法delimiter // create procedure Fsp_Plan_CheckPrj( in v_grantno varchar(32), in v_deptcode int ) begin select s.plan_code, s.plan_dept, s.plan_amount, s.exec_amount, p.cname as plan_name, d.cname as dept_name from Snap_plan_check s left join v_plan p on s.plan_code p.plan_code left join org_office d on s.plan_dept d.off_org_code group by s.plan_code, s.plan_dept, s.plan_amount, s.exec_amount, p.cname, d.cname; end // delimiter ;對應(yīng)的 mapper 就是普通selectstatementType用默認的PREPARED不需要resultMap里的游標映射。這個差異一定要在遷移前確認否則你會對著jdbcTypeCURSOR報錯半天最后發(fā)現(xiàn)是數(shù)據(jù)庫類型不對。4. 驗證請求與成功結(jié)果配置寫完后先別急著上業(yè)務(wù)代碼用最小驗證跑一遍。第一步確認 TaoToken 的 Key 有效在模型對話入口發(fā)一條測試消息能正常返回就說明憑證沒問題。第二步驗證數(shù)據(jù)庫連接和存儲過程本身用 SQL 客戶端直接call Fsp_Plan_CheckPrj(01, 1001, ?)看游標能不能出數(shù)據(jù)。第三步才是走 MyBatis。MyBatis 側(cè)的驗證建議寫一個獨立的單元測試不要混在業(yè)務(wù)鏈路里Test public void testCallProcedure() { MapString, Object params new HashMap(); params.put(grantNo, 01); params.put(offOrgCode, 1001); params.put(v_cursor, new ArrayListMapString, Object()); sqlSession.selectList(call_Fsp_Plan_CheckPrj, params); ListMapString, Object rows (ListMapString, Object) params.get(v_cursor); assertNotNull(rows); assertTrue(rows.size() 0); System.out.println(返回行數(shù): rows.size()); System.out.println(首行: rows.get(0)); }成功的結(jié)果長這樣控制臺打印出返回行數(shù)和首行內(nèi)容首行的 key 是plan_code、plan_name這些別名value 是對應(yīng)字段值。如果rows是空 List先別改代碼去 SQL 客戶端確認存儲過程本身有沒有數(shù)據(jù)。如果rows是null說明OUT參數(shù)沒被正確回填重點查jdbcType和resultMap是否寫對。提示Oracle 驅(qū)動版本不同CURSOR類型的處理方式略有差異。如果你用的是ojdbc8以上jdbcTypeCURSOR一般沒問題老版本ojdbc14可能需要換成jdbcTypeOTHER并配合typeHandler。這個坑我在老項目里踩過換驅(qū)動比改代碼省事。5. 本篇常見報錯排查5.1 ORA-01000 游標數(shù)超限報錯信息類似ORA-01000: maximum open cursors exceeded。原因是游標沒關(guān)閉每次調(diào)用都開一個新的。排查方向確認sqlSession有沒有正常關(guān)閉Spring 環(huán)境下檢查事務(wù)邊界如果用了連接池檢查連接歸還時游標是否釋放。臨時緩解可以調(diào)大數(shù)據(jù)庫的open_cursors參數(shù)但根治還是要保證會話關(guān)閉。5.2 Invalid column type這個報錯通常出現(xiàn)在OUT參數(shù)的jdbcType上。Oracle 的sys_refcursor必須寫jdbcTypeCURSOR寫成VARCHAR或OTHER都可能報錯。另外IN參數(shù)如果沒寫jdbcType某些驅(qū)動也會報這個。逐個參數(shù)補上jdbcType基本能解決。5.3 返回結(jié)果為空 List分三種情況。一是存儲過程本身沒查到數(shù)據(jù)去 SQL 客戶端驗證。二是resultMap的column和存儲過程里的列別名不一致比如存儲過程寫plan_nameresultMap寫planName映射不上就是空值。三是OUT參數(shù)的占位對象類型不對必須是new ArrayList()不能是null或String。5.4 TaoToken 側(cè) 401 或 404401 是 Key 無效或過期去 API Keys 頁面重新生成。404 是base_url寫錯確認只寫到https://taotoken.net/api不要帶多余路徑。如果兩個都排除了還是不通去接入文檔對照一下請求頭格式有些客戶端需要顯式帶Authorization: Bearer sk-xxx。5.5 MySQL 下 jdbcTypeCURSOR 報錯MySQL 沒有CURSOR這個 JDBC 類型寫了必然報錯。解決辦法是按 3.3 節(jié)的方案把存儲過程改成直接selectmapper 去掉CALLABLE和OUT游標參數(shù)。如果你必須保留OUT參數(shù)MySQL 需要用jdbcTypeOTHER配合自定義TypeHandler復(fù)雜度高不推薦。6. 把配置沉淀下來下次直接復(fù)用整套流程跑通后建議把三樣?xùn)|西沉淀成模板mapper XML 的CALLABLE骨架、Java 側(cè)的params組裝代碼、TaoToken 的settings.json/config.toml配置。下次遇到新的存儲過程只改存儲過程名、參數(shù)名和resultMap列映射其余照抄能省掉大量試錯時間。如果你在配 TaoToken 的過程中遇到 Key 或路徑問題直接去 API Keys 頁面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一個再對照接入文檔 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 檢查請求格式。長期做編碼 Agent 任務(wù)的話Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 能把額度單獨管理不會和臨時調(diào)試混在一起。模型對話調(diào)試入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 先用它確認 Key 有效再去配本地工具排查鏈路會清晰很多。最后留一個實用技巧存儲過程的resultMap列映射建議和存儲過程里的select別名逐字對照寫不要憑記憶。我見過太多空 List 的案例最后都是plan_code和planCode這種大小寫或下劃線差異導(dǎo)致的。把這兩處放在一起比對比任何調(diào)試工具都快。