ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ORACLE进阶(七)存储过程详解:从语法结构到异常处理的完整实践

ORACLE进阶(七)存储过程详解:从语法结构到异常处理的完整实践 1. 为什么业务逻辑下沉到数据库时存储过程总写不对很多人第一次写 Oracle 存储过程卡住的地方往往不是 SQL 本身而是「参数往哪传、异常怎么接、结果怎么打印出来看」。你已经有 SQL 基础能写多表关联、能写聚合但一旦要把一段业务判断封装进数据库就会遇到几个典型问题参数模式分不清 IN 和 OUT游标遍历写成了死循环SELECT INTO没查到数据直接抛NO_DATA_FOUND把整个调用链打断最后连报错信息都看不到只能靠猜。存储过程Stored Procedure本质上是把一段 PL/SQL 逻辑命名后存在数据库里调用方只需要给参数、拿结果不用关心中间怎么算。它适合谁适合那些业务规则稳定、调用频繁、又希望减少应用层与数据库之间往返的场景比如批量对账、月度汇总、状态机流转。不适合把频繁变更的业务规则塞进去那样改一次就要动数据库对象发布成本反而更高。我试过在一个对账项目里把汇总逻辑从 Java 挪进存储过程最直观的收益是原来要拉几万行到应用层再聚合现在数据库内部一次游标循环就写回结果表网络传输几乎归零。但代价是调试变难了所以这篇会把「怎么验证、怎么排错」讲透让你写完能自己跑通、自己定位问题。下面按「建表造数据 → 写过程 → 调用验证 → 排错」的顺序走每一步都给可复制的代码。你跟着敲一遍就能独立完成一个带参数模式、游标和异常捕获的完整实例。2. 前置准备TaoToken 环境与数据库连接配置在动手写过程之前先把两件事准备好一个能连上的 Oracle 实例以及一个顺手的 AI 辅助环境。我平时写 PL/SQL 会用 TaoToken 来做代码补全和报错解释尤其是遇到ORA-开头的错误码时直接把报错贴进去让它给出排查方向比翻文档快很多。TaoToken 的官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。如果你要在编辑器或命令行工具里接入需要准备三件套Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面生成Model ID 按你实际使用的模型填写。对于长期写代码、跑 Agent 的场景可以考虑 Coding Plan入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。如果只是想先验证模型能不能正常对话用模型对话页面就行https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。生成 Key 的地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。数据库这边你需要一个可写的 schema。下面所有示例都基于一张员工表和一张部门汇总表先建好-- 员工表 CREATE TABLE emp ( empno NUMBER(4) PRIMARY KEY, ename VARCHAR2(20), job VARCHAR2(20), sal NUMBER(7,2), deptno NUMBER(2) ); -- 部门汇总表 CREATE TABLE dept_summary ( deptno NUMBER(2), total_sal NUMBER(12,2), emp_count NUMBER(4), update_time DATE ); -- 造点数据 INSERT INTO emp VALUES (1001,SMITH,CLERK,800,10); INSERT INTO emp VALUES (1002,ALLEN,SALESMAN,1600,30); INSERT INTO emp VALUES (1003,WARD,SALESMAN,1250,30); INSERT INTO emp VALUES (1004,JONES,MANAGER,2975,20); INSERT INTO emp VALUES (1005,MARTIN,SALESMAN,1250,30); COMMIT;连接方式上SQL*Plus、SQL Developer、DBeaver 都可以。命令行下确认能连上sqlplus scott/tiger//127.0.0.1:1521/ORCLPDB1连上后先执行SET SERVEROUTPUT ON否则后面DBMS_OUTPUT.PUT_LINE打印的内容你看不到这是新手最常见的「代码没错但没输出」的原因。如果你用的是 SQL Developer在「DBMS Output」面板里点一下绿色加号绑定当前连接即可。环境准备好之后先确认当前用户有创建过程的权限SELECT privilege FROM user_sys_privs WHERE privilege LIKE %PROCEDURE%;如果返回空需要让 DBA 授予CREATE PROCEDURE权限。这一步别跳过否则后面CREATE OR REPLACE PROCEDURE会直接报ORA-01031: insufficient privileges。3. 可复制配置存储过程模板与参数模式详解这一节给出一个可以直接复制运行的完整存储过程包含 IN、OUT、IN OUT 三种参数模式一个显式游标以及异常捕获。先看整体结构再逐段拆解。CREATE OR REPLACE PROCEDURE proc_dept_report ( p_deptno IN emp.deptno%TYPE, -- 输入部门编号 p_min_sal IN NUMBER DEFAULT 0, -- 输入最低薪资过滤带默认值 p_total OUT NUMBER, -- 输出部门总薪资 p_count OUT NUMBER, -- 输出部门人数 p_status IN OUT VARCHAR2 -- 输入输出状态标记 ) AS -- 变量声明区 v_avg_sal NUMBER(10,2); v_row_cnt NUMBER; -- 游标声明遍历该部门所有员工 CURSOR cur_emp IS SELECT empno, ename, sal FROM emp WHERE deptno p_deptno AND sal p_min_sal ORDER BY sal DESC; v_empno emp.empno%TYPE; v_ename emp.ename%TYPE; v_sal emp.sal%TYPE; BEGIN -- 初始化输出参数 p_total : 0; p_count : 0; p_status : START; -- 先判断该部门是否存在员工避免后续空结果 SELECT COUNT(*) INTO v_row_cnt FROM emp WHERE deptno p_deptno; IF v_row_cnt 0 THEN p_status : NO_DATA; DBMS_OUTPUT.PUT_LINE(部门 || p_deptno || 没有员工记录); RETURN; END IF; -- 打开游标逐行累加 OPEN cur_emp; LOOP FETCH cur_emp INTO v_empno, v_ename, v_sal; EXIT WHEN cur_emp%NOTFOUND; p_total : p_total v_sal; p_count : p_count 1; DBMS_OUTPUT.PUT_LINE(员工 || v_ename || 薪资 || v_sal); END LOOP; CLOSE cur_emp; -- 计算平均薪资 IF p_count 0 THEN v_avg_sal : p_total / p_count; ELSE v_avg_sal : 0; END IF; -- 写回汇总表 MERGE INTO dept_summary d USING (SELECT p_deptno AS deptno FROM dual) s ON (d.deptno s.deptno) WHEN MATCHED THEN UPDATE SET d.total_sal p_total, d.emp_count p_count, d.update_time SYSDATE WHEN NOT MATCHED THEN INSERT (deptno, total_sal, emp_count, update_time) VALUES (p_deptno, p_total, p_count, SYSDATE); p_status : DONE; DBMS_OUTPUT.PUT_LINE(部门 || p_deptno || 平均薪资 || v_avg_sal); EXCEPTION WHEN NO_DATA_FOUND THEN p_status : NO_DATA_FOUND; DBMS_OUTPUT.PUT_LINE(查询未返回数据); WHEN TOO_MANY_ROWS THEN p_status : TOO_MANY_ROWS; DBMS_OUTPUT.PUT_LINE(查询返回多行请检查条件); WHEN OTHERS THEN p_status : ERROR: || SQLCODE; DBMS_OUTPUT.PUT_LINE(过程执行出错 || SQLERRM); ROLLBACK; END proc_dept_report; /参数模式是这段代码的核心。IN 是默认模式只能传入不能赋值适合做查询条件OUT 只能在过程体内部赋值用来把结果带回调用方IN OUT 既能传入初始值又能被过程修改后带出。上面p_status用 IN OUT是因为调用方可能先传一个初始状态进来过程再把它改成DONE或错误码。游标部分用的是显式游标加LOOP ... FETCH ... EXIT WHEN的经典写法。cur_emp%NOTFOUND是游标属性表示 fetch 不到记录了。注意EXIT WHEN必须放在FETCH之后否则会多处理一行或漏掉最后一行。如果你更喜欢简洁写法可以用游标 FOR 循环FOR rec IN cur_emp LOOP p_total : p_total rec.sal; p_count : p_count 1; END LOOP;FOR 循环会自动打开、fetch、关闭游标不用手动管理出错概率更低。我一般在对性能要求不极端时优先用 FOR 循环。异常处理块里NO_DATA_FOUND和TOO_MANY_ROWS是最常见的两个预定义异常。SELECT INTO没查到数据抛前者返回多行抛后者。WHEN OTHERS是兜底配合SQLCODE和SQLERRM能拿到错误码和描述。注意WHEN OTHERS里做了ROLLBACK这是为了保证过程失败时事务不残留半成品数据。如果你在编辑器里接入 TaoToken 做补全配置文件可以这样写以常见 settings 为例{ baseUrl: https://taotoken.net/api, apiKey: 你的_API_KEY, model: 你的_MODEL_ID }三件套缺一不可Base URL 指向https://taotoken.net/apiAPI Key 从控制台生成Model ID 按实际模型填。填错 Base URL 会直接连不上填错 Model ID 会报模型不存在。4. 验证请求调用存储过程并查看执行结果过程建好之后怎么确认它真的按预期跑了分三步匿名块调用、命令行调用、数据字典查询。先看匿名块调用这是最灵活的验证方式SET SERVEROUTPUT ON; DECLARE v_total NUMBER; v_count NUMBER; v_status VARCHAR2(50) : INIT; BEGIN proc_dept_report( p_deptno 30, p_min_sal 1000, p_total v_total, p_count v_count, p_status v_status ); DBMS_OUTPUT.PUT_LINE(总薪资 || v_total); DBMS_OUTPUT.PUT_LINE(人数 || v_count); DBMS_OUTPUT.PUT_LINE(状态 || v_status); END; /这里用了命名参数传递p_deptno 30顺序可以打乱可读性比按位置传参好。执行后你应该看到类似输出员工 ALLEN 薪资 1600 员工 WARD 薪资 1250 员工 MARTIN 薪资 1250 部门 30 平均薪资 1366.67 总薪资4100 人数3 状态DONE注意p_min_sal传了 1000所以薪资 800 的 SMITH 不在 30 部门本来也不影响但如果你传 2000就会过滤掉 1600 以下的人数变成 0此时过程会走p_count 0分支平均薪资置 0状态仍是DONE。你可以改参数多跑几次观察输出变化。命令行方式更简洁适合无返回值的场景-- 无返回值调用 EXEC proc_dept_report(30, 0, :total, :cnt, :status); -- 或者用 CALL VAR v_total NUMBER; VAR v_count NUMBER; VAR v_status VARCHAR2(50); EXEC proc_dept_report(30, 0, :v_total, :v_count, :v_status); PRINT v_total v_count v_status;VAR声明绑定变量EXEC执行PRINT输出。这种方式在 SQL*Plus 里很顺手但注意绑定变量在会话结束后就没了。验证数据是否真的写进汇总表SELECT deptno, total_sal, emp_count, TO_CHAR(update_time,YYYY-MM-DD HH24:MI:SS) AS upd FROM dept_summary WHERE deptno 30;应该能看到一行30 | 4100 | 3 | 当前时间。如果没写进去先检查过程里 MERGE 语句的 ON 条件是否匹配再确认有没有被异常块 ROLLBACK 掉。查数据字典确认过程已注册-- 查过程基本信息 SELECT object_name, object_type, status, created FROM user_objects WHERE object_type PROCEDURE AND object_name PROC_DEPT_REPORT; -- 查过程参数 SELECT argument_name, position, in_out, data_type, defaulted FROM user_arguments WHERE object_name PROC_DEPT_REPORT ORDER BY position; -- 查源码如果没加密 SELECT line, text FROM user_source WHERE name PROC_DEPT_REPORT ORDER BY line;user_objects里的status如果是INVALID说明过程编译没通过需要重新编译ALTER PROCEDURE proc_dept_report COMPILE;编译后仍 INVALID通常是依赖的表结构变了或权限不足查user_errors看具体原因SELECT line, position, text FROM user_errors WHERE name PROC_DEPT_REPORT;这一步是排错的关键user_errors会直接告诉你哪一行、什么错误比盲目改代码高效得多。5. 常见报错排查从 ORA-01403 到 ORA-06550写存储过程时遇到的报错八成集中在这几个。下面按真实错误信息对照排查。ORA-01403: no data found。这是SELECT INTO没查到数据时抛的。比如SELECT sal INTO v_sal FROM emp WHERE empno 9999;empno 9999 不存在直接抛NO_DATA_FOUND。解决办法有两个一是先用COUNT(*)判断存在性再查二是用异常块捕获。上面模板里两种都用了。如果你不想让它抛异常也可以写成BEGIN SELECT sal INTO v_sal FROM emp WHERE empno 9999; EXCEPTION WHEN NO_DATA_FOUND THEN v_sal : 0; END;ORA-01422: exact fetch returns more than requested number of rows。SELECT INTO返回多行时抛TOO_MANY_ROWS。比如按 deptno 查 sal一个部门多人就会触发。要么加AND ROWNUM 1要么改用游标遍历。ORA-06550 / PLS-00201: identifier must be declared。这通常是过程名拼错、参数名不对或者调用的过程不在当前 schema 下。检查user_objects里有没有这个名字跨 schema 调用要加schema_name.proc_name。ORA-01031: insufficient privileges。创建过程权限不足让 DBA 授予CREATE PROCEDURE。如果是调用别人的过程需要EXECUTE权限GRANT EXECUTE ON other_schema.proc_name TO your_user;ORA-06502: numeric or value error。变量长度不够或类型不匹配。比如p_status VARCHAR2(5)却赋值NO_DATA_FOUND直接溢出。把变量声明放宽或者用%TYPE跟随列类型。local proxy failed / 401。如果你在通过 API 调用模型辅助写代码时遇到这类报错先检查 Base URL 是否写成https://taotoken.net/apiAPI Key 是否有效Model ID 是否填对。401 一般是 Key 无效或没带认证头local proxy failed 通常是本地代理配置和实际地址不一致。这三件套Base URL Key Model ID任何一项错都会连不上。OAuth 相关报错。如果你用的是 Claude Code 这类工具接入遇到 OAuth 报错检查授权流程是否走完token 是否过期。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 有详细说明。DBMS_OUTPUT 没输出。九成是忘了SET SERVEROUTPUT ON或者缓冲区太小。可以调大SET SERVEROUTPUT ON SIZE UNLIMITED;过程编译通过但执行报 ORA-06512。这是异常堆栈的行号提示配合user_errors和DBMS_OUTPUT里的SQLERRM一起看能定位到具体哪一行。我一般会在异常块里把SQLCODE和SQLERRM都打出来再配合DBMS_UTILITY.FORMAT_ERROR_BACKTRACE拿到精确行号EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(错误码 || SQLCODE); DBMS_OUTPUT.PUT_LINE(错误信息 || SQLERRM); DBMS_OUTPUT.PUT_LINE(堆栈 || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE); ROLLBACK;这个 backtrace 在复杂过程里特别有用能直接告诉你第几行出的问题省去大量猜测时间。6. 把存储过程接入日常开发流到这里你已经能独立写出带参数模式、游标、异常捕获的存储过程并且知道怎么用DBMS_OUTPUT和数据字典验证结果、怎么对照报错定位问题。最后说几个实战里能省时间的习惯。第一过程命名带前缀比如proc_、pkg_方便在user_objects里过滤。第二每个过程都加CREATE OR REPLACE避免重复建报错。第三异常块里一定打SQLERRM和 backtrace别只写WHEN OTHERS THEN NULL那等于把问题藏起来。第四改完过程记得ALTER PROCEDURE ... COMPILE并查user_errors别假设它一定编译通过。如果你在写 PL/SQL 时想让 AI 帮你解释报错或补全游标逻辑可以用 TaoToken 的模型对话先验证思路https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。需要生成 API Key 走 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。长期在数据库和代码之间来回切换的话Coding Plan 会更顺手https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。把上面那个proc_dept_report跑通之后试着改一改把游标换成带参数的游标把p_min_sal的默认值去掉强制传入再故意传一个不存在的部门号看NO_DATA分支怎么走。改三次、跑三次比看十遍语法记得牢。
返回列表