69、Oracle PL/SQL调用:匿名块、存储过程、函数、包(Package)、游标处理

说实话,PL/SQL 这东西,我早年刚接触时觉得挺别扭的。明明 SQL 就能查数据,干嘛还要搞一套编程语言?后来在项目中做复杂业务逻辑,才明白它的价值——把逻辑写在数据库里,减少网络往返,性能直接起飞

今天咱们就聊聊 Oracle PL/SQL 的几种调用方式。我按自己的经验,把它们分成五个层次:匿名块、存储过程、函数、包、游标。每个都有它的用武之地。

一、匿名块:快速验证的利器

匿名块,说白了就是一段没有名字的 PL/SQL 代码。写完就扔,不存数据库。

我经常用它做两件事:一是快速测试某个逻辑对不对,二是在脚本里临时执行一段逻辑。

DECLARE
   v_count NUMBER;
BEGIN
   SELECT COUNT(*) INTO v_count FROM employees;
   DBMS_OUTPUT.PUT_LINE('员工总数: ' || v_count);
END;
/

嗯,这里要注意:DBMS_OUTPUT 的输出,你得在客户端先开启 SET SERVEROUTPUT ON,不然啥也看不到。我刚开始就吃过这个亏,调试了半天以为代码写错了。

我的习惯:匿名块适合做一次性任务。如果你发现某段逻辑要反复用,赶紧改成存储过程或函数。

二、存储过程:封装业务逻辑

存储过程是 PL/SQL 的核心。它没有返回值,但可以通过 OUT 参数往外传数据。

我在项目中遇到过这样一个场景:每天凌晨要批量更新订单状态。用存储过程封装,定时任务一调就完事。

CREATE OR REPLACE PROCEDURE update_order_status (
   p_order_id   IN  NUMBER,
   p_new_status IN  VARCHAR2,
   p_result     OUT VARCHAR2
) IS
BEGIN
   UPDATE orders SET status = p_new_status WHERE order_id = p_order_id;
   
   IF SQL%ROWCOUNT = 0 THEN
      p_result := '订单不存在';
   ELSE
      p_result := '更新成功';
   END IF;
   COMMIT;
EXCEPTION
   WHEN OTHERS THEN
      ROLLBACK;
      p_result := '错误: ' || SQLERRM;
END;
/

调用方式很简单:

DECLARE
   v_res VARCHAR2(100);
BEGIN
   update_order_status(1001, '已发货', v_res);
   DBMS_OUTPUT.PUT_LINE(v_res);
END;
/
我曾经踩过的坑:存储过程里如果忘了写 COMMIT,调用方也不知道。后来我养成了习惯——存储过程内部尽量不 COMMIT,让调用方控制事务。这样更灵活,也更容易回滚。

三、函数:必须返回一个值

函数和存储过程很像,区别在于函数必须有 RETURN 子句。说白了,函数就是「你给我参数,我还你一个结果」。

我一般用函数做计算或查询,比如根据员工 ID 算年终奖:

CREATE OR REPLACE FUNCTION calc_bonus (
   p_emp_id   IN  NUMBER,
   p_year     IN  NUMBER
) RETURN NUMBER IS
   v_salary   NUMBER;
   v_bonus    NUMBER;
BEGIN
   SELECT salary INTO v_salary FROM employees WHERE employee_id = p_emp_id;
   
   v_bonus := v_salary * 0.15;
   -- 假设每年按15%发奖金
   RETURN v_bonus;
EXCEPTION
   WHEN NO_DATA_FOUND THEN
      RETURN 0;
END;
/

调用时可以直接用在 SQL 里:

SELECT employee_id, calc_bonus(employee_id, 2024) AS bonus FROM employees;

你想想看,这比在应用层循环计算快了多少倍?

四、包(Package):把相关功能打包

包是 PL/SQL 里我最喜欢的东西。它把相关的存储过程、函数、变量、游标打包在一起,就像一个工具箱。

包分两部分:包规范(声明)和包体(实现)。

先看包规范:

CREATE OR REPLACE PACKAGE emp_pkg IS
   -- 公共变量
   g_dept_id   NUMBER := 10;
   
   -- 函数声明
   FUNCTION get_emp_count RETURN NUMBER;
   
   -- 存储过程声明
   PROCEDURE list_emps_by_dept(p_dept_id IN NUMBER);
   
   -- 游标声明
   CURSOR c_emp_cur IS SELECT * FROM employees;
END emp_pkg;
/

再看包体:

CREATE OR REPLACE PACKAGE BODY emp_pkg IS
   
   FUNCTION get_emp_count RETURN NUMBER IS
      v_cnt NUMBER;
   BEGIN
      SELECT COUNT(*) INTO v_cnt FROM employees;
      RETURN v_cnt;
   END;
   
   PROCEDURE list_emps_by_dept(p_dept_id IN NUMBER) IS
   BEGIN
      FOR rec IN (SELECT * FROM employees WHERE department_id = p_dept_id) LOOP
         DBMS_OUTPUT.PUT_LINE(rec.last_name || ' - ' || rec.salary);
      END LOOP;
   END;
   
END emp_pkg;
/

调用时用 包名.成员名

BEGIN
   DBMS_OUTPUT.PUT_LINE('员工总数: ' || emp_pkg.get_emp_count);
   emp_pkg.list_emps_by_dept(20);
END;
/
我的建议:项目里尽量用包来组织代码。一个模块一个包,维护起来清晰得多。我见过有些项目把所有存储过程散落在数据库里,找起来跟大海捞针似的。

五、游标处理:逐行操作的艺术

游标,说白了就是指向结果集的一个指针。你用它一行一行地处理数据。

Oracle 里有三种游标:隐式游标显式游标REF CURSOR

隐式游标是 Oracle 自动管理的,比如 SELECT INTO 就用了隐式游标。显式游标需要你手动声明、打开、提取、关闭。

我重点说说显式游标和 REF CURSOR。

5.1 显式游标

DECLARE
   CURSOR emp_cur IS 
      SELECT employee_id, last_name, salary 
      FROM employees 
      WHERE department_id = 50;
   
   v_emp_id   employees.employee_id%TYPE;
   v_name     employees.last_name%TYPE;
   v_salary   employees.salary%TYPE;
BEGIN
   OPEN emp_cur;
   LOOP
      FETCH emp_cur INTO v_emp_id, v_name, v_salary;
      EXIT WHEN emp_cur%NOTFOUND;
      
      DBMS_OUTPUT.PUT_LINE(v_emp_id || ' - ' || v_name || ' - ' || v_salary);
   END LOOP;
   CLOSE emp_cur;
END;
/

FOR 循环 更简洁,Oracle 会自动帮你打开和关闭游标:

BEGIN
   FOR rec IN (SELECT employee_id, last_name FROM employees) LOOP
      DBMS_OUTPUT.PUT_LINE(rec.employee_id || ' - ' || rec.last_name);
   END LOOP;
END;
/

5.2 REF CURSOR:动态游标

REF CURSOR 更灵活,你可以在运行时决定查询什么。我一般在做报表或动态查询时用它。

CREATE OR REPLACE PROCEDURE get_emps_by_dept (
   p_dept_id IN NUMBER,
   p_cursor  OUT SYS_REFCURSOR
) IS
BEGIN
   OPEN p_cursor FOR
      SELECT employee_id, last_name, salary 
      FROM employees 
      WHERE department_id = p_dept_id;
END;
/

调用方:

DECLARE
   v_cursor SYS_REFCURSOR;
   v_id     NUMBER;
   v_name   VARCHAR2(100);
   v_sal    NUMBER;
BEGIN
   get_emps_by_dept(50, v_cursor);
   
   LOOP
      FETCH v_cursor INTO v_id, v_name, v_sal;
      EXIT WHEN v_cursor%NOTFOUND;
      DBMS_OUTPUT.PUT_LINE(v_id || ' - ' || v_name || ' - ' || v_sal);
   END LOOP;
   CLOSE v_cursor;
END;
/
我曾经犯过的错:游标用完不关闭,导致数据库连接池被耗尽。后来我强制自己——写 OPEN 的同时就写 CLOSE,就像 malloc 和 free 一样,成对出现

六、知识体系总览

下面这张图,是我自己梳理的 PL/SQL 调用体系。你看一眼就能明白它们之间的关系。

PL/SQL 调用体系 PL/SQL 调用 匿名块 存储过程 函数 游标 一次性执行 OUT 参数 RETURN 值 模块封装 逐行处理 核心原则 1. 匿名块用于快速验证,用完即弃 2. 存储过程封装逻辑,函数返回结果 3. 包组织代码,游标处理逐行数据

七、总结一下

PL/SQL 的这五种调用方式,各有各的定位:

  • 匿名块:临时用,不保存
  • 存储过程:做事情,不返回值
  • 函数:做计算,必须返回值
  • :把相关功能打包,方便管理
  • 游标:逐行处理结果集

我个人建议,在实际项目中优先用包来组织代码。哪怕一开始只有一个存储过程,也放进包里。等后面功能多了,你会发现这个决定有多明智。

好了,今天就聊到这儿。记住一点:PL/SQL 不是银弹,但在数据库端做逻辑处理时,它确实是最趁手的工具


公众号:蓝海资料掘金营,微信deep3321