注册
DM的游标类型以及使用解析
专栏/技术分享/ 文章详情 /

DM的游标类型以及使用解析

anon 2025/09/26 163 0 0
摘要

在 DM8 数据库开发中,处理单行数据时,SELECT...INTO语句足以满足需求 —— 例如查询某个员工的工资、获取某张表的记录数。但面对多行结果集仅靠基础 SQL 语句就显得力不从心。此时,游标(Cursor) 作为 DMSQL 程序的核心工具,成为解决多行数据处理问题的关键。

游标本质是数据库服务器为查询结果集创建的临时指针,它允许开发者逐条遍历结果集中的记录,实现复杂的业务逻辑(如条件判断、数据修改、跨表关联)。

一、游标的核心概念

1.1 游标与静态 SQL 的局限性

  • 静态 SQL(如SELECT...INTO)仅能处理单行结果,若查询返回多行,会触发TOO_MANY_ROWS异常;若无数据,会触发NO_DATA_FOUND异常。
  • 游标则支持多行结果集遍历,开发者可通过游标逐行读取数据,灵活实现 “判断 - 处理 - 跳转” 等业务逻辑(例如:遍历员工表,为薪资低于 5000 的员工加薪 10%)。

1.2 游标的本质与生命周期

游标本质是结果集的内存映射

其生命周期包含 4 个阶段:

  1. 定义(Declare):声明游标与关联的查询语句(如CURSOR cur IS SELECT * FROM EMPLOYEE),此时仅存储查询逻辑,未执行。
  2. 打开(Open):执行关联的查询语句,将结果集加载到内存,游标指向结果集的 “第一行之前”。
  3. 提取(Fetch):游标向下移动一行,将当前行数据存入变量,开发者可对数据进行处理。
  4. 关闭(Close):释放游标占用的内存与资源,游标失效。

1.3 游标分类与选型建议

DM8 将游标分为 4 类,不同类型适用于不同场景,选型直接影响代码效率与可维护性:

游标类型 核心特点 适用场景 优点 缺点
静态游标(隐式) 系统自动管理,无需手动声明 / 关闭 简单 DML / 查询(如UPDATE后判断影响行数) 代码简洁,无需手动控制生命周期 灵活性低,无法处理多行查询
静态游标(显式) 手动声明 / 打开 / 提取 / 关闭,查询固定 固定查询逻辑的多行处理(如批量统计) 可控性强,支持游标属性判断 代码量多,需手动管理资源
动态游标 打开时才绑定查询语句,支持动态 SQL 查询逻辑不确定(如表名 / 列名由参数决定) 灵活性高,适配动态场景 无法提前编译,性能略低
游标变量 / 引用游标 指向游标对象的指针,支持结果集传递 子程序间共享结果集(如存储过程返回多行数据) 可传递、可复用 需理解指针逻辑,调试难度高

选型思考

  • 若仅需判断 DML 影响行数(如UPDATE后确认是否修改成功),用隐式静态游标
  • 若查询逻辑固定(如每月薪资统计),用显式静态游标
  • 若查询条件动态变化(如用户输入部门 ID 查询员工),用动态游标
  • 若需在存储过程 / 函数间传递多行结果(如子过程处理结果集后返回给主程序),用游标变量 / 引用游标

二、静态游标

静态游标是最常用的游标类型,其查询逻辑在编译时确定。分为隐式游标显式游标

2.1 隐式静态游标

可通过隐式游标属性获取执行信息,核心属性如下:

  • SQL%FOUND:若语句影响 / 查询到数据,返回TRUE;否则FALSE
  • SQL%NOTFOUND:与SQL%FOUND相反;
  • SQL%ROWCOUNT:DML 语句影响的行数,或SELECT...INTO返回的行数;
  • SQL%ISOPEN:始终为FALSE(系统执行完语句后立即关闭游标)。

示例:用隐式游标判断 DML 执行结果

更新 “程序员” 职位的员工薪资(加薪 5%),并判断是否更新成功。

-- 隐式游标:判断UPDATE执行结果 BEGIN -- 执行DML语句,系统自动创建隐式游标SQL UPDATE RESOURCES.EMPLOYEE SET SALARY = SALARY * 1.05 -- 加薪5% WHERE TITLE = '程序员'; -- 通过隐式游标属性判断结果 IF SQL%FOUND THEN DBMS_OUTPUT.PUT_LINE('薪资更新成功!'); DBMS_OUTPUT.PUT_LINE('共更新 ' || SQL%ROWCOUNT || ' 名员工'); COMMIT; -- 确认更新 ELSE DBMS_OUTPUT.PUT_LINE('未找到职位为"程序员"的员工,无需更新'); ROLLBACK; -- 回滚空操作 END IF; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('更新异常:' || SQLCODE || ' - ' || SQLERRM); ROLLBACK; END;

优缺点:

  • 优点:无需手动声明 / 关闭,代码简洁,适合简单场景;
  • 缺点:仅能获取 “整体执行结果”(如总影响行数),无法逐行处理数据;若需遍历多行,必须用显式游标。

2.2 显式静态游标

当需要逐行处理多行数据时,显式静态游标是首选。步骤为:定义→打开→提取→关闭

2.2.1 示例:显式游标遍历员工数据,批量生成薪资报告

遍历 “技术部门” 员工,打印姓名、职位、薪资,并统计平均薪资。

-- 显式游标:批量生成技术部门薪资报告 DECLARE -- 1. 定义显式游标:关联技术部门员工查询 CURSOR tech_emp_cursor IS SELECT NAME, TITLE, SALARY FROM RESOURCES.EMPLOYEE WHERE MANAGERID = 4 -- 技术经理的下属(MANAGERID=4) ORDER BY SALARY DESC; -- 定义变量存储游标数据 v_name VARCHAR(50); v_title VARCHAR(50); v_salary DEC(19,4); -- 统计变量 v_total_salary DEC(19,4) := 0; -- 总薪资 v_emp_count INT := 0; -- 员工数量 v_avg_salary DEC(19,4); -- 平均薪资 BEGIN -- 2. 打开游标 OPEN tech_emp_cursor; -- 判断游标是否打开成功 IF tech_emp_cursor%ISOPEN THEN DBMS_OUTPUT.PUT_LINE('================ 技术部门薪资报告 ================'); DBMS_OUTPUT.PUT_LINE('姓名 职位 薪资'); DBMS_OUTPUT.PUT_LINE('----------------------------------------'); -- 3. 提取数据:循环遍历,直到无数据 LOOP FETCH tech_emp_cursor INTO v_name, v_title, v_salary; EXIT WHEN tech_emp_cursor%NOTFOUND; -- 退出条件:无数据 -- 处理数据:打印与统计 DBMS_OUTPUT.PUT_LINE(RPAD(v_name, 10) || RPAD(v_title, 10) || v_salary); v_total_salary := v_total_salary + v_salary; v_emp_count := v_emp_count + 1; END LOOP; -- 计算平均薪资 IF v_emp_count > 0 THEN v_avg_salary := v_total_salary / v_emp_count; DBMS_OUTPUT.PUT_LINE('----------------------------------------'); DBMS_OUTPUT.PUT_LINE('部门总人数:' || v_emp_count); DBMS_OUTPUT.PUT_LINE('总薪资:' || v_total_salary); DBMS_OUTPUT.PUT_LINE('平均薪资:' || ROUND(v_avg_salary, 2)); ELSE DBMS_OUTPUT.PUT_LINE('----------------------------------------'); DBMS_OUTPUT.PUT_LINE('技术部门无员工数据'); END IF; ELSE RAISE_APPLICATION_ERROR(-20001, '游标打开失败!'); END IF; -- 4. 关闭游标,释放资源 CLOSE tech_emp_cursor; EXCEPTION WHEN INVALID_CURSOR THEN DBMS_OUTPUT.PUT_LINE('游标异常:无效的游标操作(可能未打开或已关闭)'); -- 异常时确保游标关闭(避免资源泄漏) IF tech_emp_cursor%ISOPEN THEN CLOSE tech_emp_cursor; END IF; WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('其他异常:' || SQLCODE || ' - ' || SQLERRM); IF tech_emp_cursor%ISOPEN THEN CLOSE tech_emp_cursor; END IF; END;

2.2.2 显式游标属性的关键用法

在示例中,我们用到了%ISOPEN%NOTFOUND%ROWCOUNT三个核心属性,它们是控制游标逻辑的关键:

属性 作用场景 注意事项
%ISOPEN 判断游标是否打开(避免重复打开 / 关闭) 打开前为FALSE,打开后为TRUE,关闭后为FALSE
%NOTFOUND 循环退出条件(无数据可提取时为TRUE 首次FETCH前为NULL,需用EXIT WHEN判断
%ROWCOUNT 统计已提取的行数(如报告中的员工数量) 首次FETCH前为0,每次FETCH成功后 + 1
%FOUND 判断是否提取到数据(与%NOTFOUND相反) 可替代%NOTFOUND,但逻辑需调整(EXIT WHEN NOT %FOUND

三、动态游标

静态游标的查询语句在编译时固定,但若查询逻辑不确定(如表名、列名由用户输入决定),静态游标就无法满足需求。此时需用动态游标—— 在 “打开游标” 时才绑定查询语句,实现动态 SQL 的逐行处理。

3.1 动态游标的核心语法

动态游标的关键是OPEN...FOR...USING语法,通过USING子句绑定参数,避免 SQL 注入:

-- 1. 定义动态游标(仅声明类型,不绑定查询) CURSOR 动态游标名; -- 2. 构建动态SQL语句(含参数占位符?) v_sql VARCHAR(1000) := 'SELECT1, 列2 FROM 表名 WHERE 条件 = ?'; -- 3. 打开动态游标,绑定SQL与参数 OPEN 动态游标名 FOR v_sql USING 参数1; -- 4. 提取数据(与静态游标一致) FETCH 动态游标名 INTO 变量1, 变量2; -- 5. 关闭游标 CLOSE 动态游标名;

关键注意事项

  • 动态 SQL 中的参数必须用?作为占位符,不可直接拼接字符串(避免 SQL 注入);
  • USING子句的参数个数、类型必须与?完全匹配,否则触发INVALID_NUMBER等异常;

3.2 示例:动态游标实现 “按部门查询员工”

用户传入部门经理 ID(p_managerid),动态查询该经理下属的员工信息(姓名、薪资、职位),支持模糊查询职位。

-- 动态游标:按部门经理ID查询下属员工 CREATE OR REPLACE PROCEDURE query_emp_by_manager( p_managerid INT, -- 输入参数:部门经理ID p_title_like VARCHAR(50) -- 输入参数:职位模糊查询(如'经理'、'代表') ) AS -- 1. 定义动态游标 cur_dynamic CURSOR; -- 变量:存储查询结果与动态SQL v_name VARCHAR(50); v_salary DEC(19,4); v_title VARCHAR(50); v_sql VARCHAR(1000); -- 动态SQL语句 BEGIN -- 2. 构建动态SQL(用?作为参数占位符) v_sql := 'SELECT NAME, SALARY, TITLE FROM RESOURCES.EMPLOYEE WHERE MANAGERID = ? -- 第一个参数:经理ID AND TITLE LIKE ? -- 第二个参数:职位模糊查询 ORDER BY SALARY DESC'; -- 3. 打开动态游标,绑定SQL与参数(USING子句顺序需与?一致) OPEN cur_dynamic FOR v_sql USING p_managerid, '%' || p_title_like || '%'; -- 拼接模糊查询符号% -- 4. 提取数据 DBMS_OUTPUT.PUT_LINE('---------------- 部门经理ID=' || p_managerid || ' 的下属员工 ----------------'); DBMS_OUTPUT.PUT_LINE('姓名 薪资 职位'); DBMS_OUTPUT.PUT_LINE('----------------------------------------'); LOOP FETCH cur_dynamic INTO v_name, v_salary, v_title; EXIT WHEN cur_dynamic%NOTFOUND; DBMS_OUTPUT.PUT_LINE(RPAD(v_name, 10) || RPAD(v_salary, 12) || v_title); END LOOP; -- 处理无数据场景 IF cur_dynamic%ROWCOUNT = 0 THEN DBMS_OUTPUT.PUT_LINE('----------------------------------------'); DBMS_OUTPUT.PUT_LINE('未找到符合条件的员工'); END IF; -- 5. 关闭游标 CLOSE cur_dynamic; EXCEPTION WHEN INVALID_CURSOR THEN DBMS_OUTPUT.PUT_LINE('游标异常:无效的游标操作'); IF cur_dynamic%ISOPEN THEN CLOSE cur_dynamic; END IF; WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('查询异常:' || SQLCODE || ' - ' || SQLERRM); IF cur_dynamic%ISOPEN THEN CLOSE cur_dynamic; END IF; END; -- 调用存储过程:查询经理ID=1(总经理)下属的“经理”职位员工 CALL query_emp_by_manager(1, '经理');

为什么推荐使用动态游标

动态 SQL 若直接拼接参数(如v_sql := 'WHERE MANAGERID = ' || p_managerid),会面临 SQL 注入风险。例如:若用户传入p_managerid = '1 OR 1=1',拼接后的 SQL 会变成WHERE MANAGERID = 1 OR 1=1,导致查询所有员工,泄露敏感数据。

USING子句通过参数绑定避免了这一问题 —— 参数会被当作 “值” 而非 “SQL 片段” 处理,即使传入恶意字符串,也只会作为条件值匹配,不会执行恶意 SQL。

结论:动态游标必须使用USING子句绑定参数,禁止直接拼接 SQL 字符串。

四、游标变量与引用变量

在复杂业务中,常需在子程序(存储过程 / 函数)间传递多行结果集(如:子过程 A 查询员工数据,子过程 B 处理数据,主程序 C 汇总结果)。此时,普通游标无法满足需求,需用游标变量引用游标

4.1 游标变量:指向游标的 “指针”

游标变量本质是指向游标对象的指针,它不存储结果集,仅存储游标地址。多个游标变量可指向同一游标,实现结果集共享。

4.1.1 游标变量的核心语法

-- 1. 定义游标变量(两种方式) -- 方式1:直接赋值为已定义的游标 CURSOR 源游标名 IS SELECT ...; 游标变量名 CURSOR := 源游标名; -- 方式2:打开时动态绑定查询(与动态游标类似) 游标变量名 CURSOR; OPEN 游标变量名 FOR SELECT ... USING 参数

4.1.2 示例:游标变量在子程序间传递结果集

  • 主程序:调用子过程get_emp_cursor获取员工游标变量;
  • 子过程:返回 “薪资大于 60000” 的员工游标变量;
  • 主程序:遍历游标变量,打印员工信息。
-- 子过程:返回薪资大于指定值的员工游标变量 CREATE OR REPLACE PROCEDURE get_emp_cursor( p_min_salary DEC(19,4), -- 输入参数:最小薪资 p_emp_cursor OUT CURSOR -- 输出参数:游标变量(传递结果集) ) AS BEGIN -- 打开游标变量,绑定查询语句 OPEN p_emp_cursor FOR SELECT EMPLOYEEID, NAME, SALARY FROM RESOURCES.EMPLOYEE WHERE SALARY > p_min_salary ORDER BY SALARY DESC; END; / -- 主程序:调用子过程获取游标变量,遍历结果 DECLARE -- 定义游标变量(接收子过程返回的结果集) v_emp_cursor CURSOR; v_empid INT; v_name VARCHAR(50); v_salary DEC(19,4); BEGIN -- 调用子过程,获取薪资>60000的员工游标变量 CALL get_emp_cursor(60000, v_emp_cursor); -- 遍历游标变量 DBMS_OUTPUT.PUT_LINE('薪资大于60000的员工列表:'); DBMS_OUTPUT.PUT_LINE('工号 | 姓名 | 薪资'); DBMS_OUTPUT.PUT_LINE('------------------------'); LOOP FETCH v_emp_cursor INTO v_empid, v_name, v_salary; EXIT WHEN v_emp_cursor%NOTFOUND; DBMS_OUTPUT.PUT_LINE(v_empid || ' | ' || v_name || ' | ' || v_salary); END LOOP; -- 关闭游标变量 CLOSE v_emp_cursor; EXCEPTION WHEN INVALID_CURSOR THEN DBMS_OUTPUT.PUT_LINE('游标变量异常:无效操作'); IF v_emp_cursor%ISOPEN THEN CLOSE v_emp_cursor; END IF; WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('主程序异常:' || SQLCODE || ' - ' || SQLERRM); IF v_emp_cursor%ISOPEN THEN CLOSE v_emp_cursor; END IF; END;

4.2 引用游标:用 REF CURSOR 定义结果集类型

引用游标(REF CURSOR)通过定义 “游标类型”,明确结果集结构,提高代码可读性与安全性。

4.2.1 引用游标的核心语法

-- 1. 定义REF CURSOR类型(指定结果集结构) TYPE 游标类型名 IS REF CURSOR RETURN 表名%ROWTYPE; -- 与表结构一致 -- 或指定自定义记录类型 TYPE 记录类型名 IS RECORD(列1 类型1, 列2 类型2); TYPE 游标类型名 IS REF CURSOR RETURN 记录类型名; -- 2. 声明引用游标变量 引用游标变量名 游标类型名; -- 3. 打开引用游标(与动态游标一致) OPEN 引用游标变量名 FOR SELECT ...;

4.2.2 示例:引用游标返回自定义结果集

需求:创建存储过程,返回 “员工姓名、部门(通过 MANAGERID 关联)、薪资” 的自定义结果集,用引用游标传递。

-- 1. 定义REF CURSOR类型(自定义结果集结构) CREATE OR REPLACE PACKAGE emp_package AS -- 自定义记录类型:员工信息(含部门) TYPE emp_dept_record IS RECORD( emp_name VARCHAR(50), -- 员工姓名 dept_name VARCHAR(50), -- 部门名称(根据MANAGERID映射) salary DEC(19,4) -- 薪资 ); -- 定义REF CURSOR类型,返回自定义记录 TYPE emp_dept_cursor IS REF CURSOR RETURN emp_dept_record; -- 存储过程:返回员工-部门关联结果集 PROCEDURE get_emp_dept_cursor( p_dept_name_like VARCHAR(50), -- 输入参数:部门名称模糊查询 p_result_cursor OUT emp_dept_cursor -- 输出参数:引用游标 ); END emp_package; / -- 2. 实现存储过程 CREATE OR REPLACE PACKAGE BODY emp_package AS PROCEDURE get_emp_dept_cursor( p_dept_name_like VARCHAR(50), p_result_cursor OUT emp_dept_cursor ) AS BEGIN -- 打开引用游标,关联查询(映射部门名称) OPEN p_result_cursor FOR SELECT e.NAME AS emp_name, -- 根据MANAGERID映射部门名称 CASE WHEN e.MANAGERID IS NULL THEN '总经办' WHEN e.MANAGERID = 1 THEN '销售部/技术部' WHEN e.MANAGERID = 2 THEN '销售部' WHEN e.MANAGERID = 4 THEN '技术部' ELSE '其他部门' END AS dept_name, e.SALARY FROM RESOURCES.EMPLOYEE e WHERE CASE WHEN e.MANAGERID IS NULL THEN '总经办' WHEN e.MANAGERID = 1 THEN '销售部/技术部' WHEN e.MANAGERID = 2 THEN '销售部' WHEN e.MANAGERID = 4 THEN '技术部' ELSE '其他部门' END LIKE '%' || p_dept_name_like || '%' ORDER BY e.SALARY DESC; END get_emp_dept_cursor; END emp_package; / -- 3. 主程序:调用存储过程,遍历引用游标 DECLARE -- 声明引用游标变量 v_result_cursor emp_package.emp_dept_cursor; -- 声明变量存储自定义记录 v_emp_dept emp_package.emp_dept_record; BEGIN -- 调用存储过程,查询“销售部”相关员工 emp_package.get_emp_dept_cursor('销售部', v_result_cursor); DBMS_OUTPUT.PUT_LINE('---------------- 销售部相关员工 ----------------'); DBMS_OUTPUT.PUT_LINE('员工姓名 | 部门名称 | 薪资'); DBMS_OUTPUT.PUT_LINE('----------------------------------------'); -- 遍历引用游标 LOOP FETCH v_result_cursor INTO v_emp_dept; EXIT WHEN v_result_cursor%NOTFOUND; DBMS_OUTPUT.PUT_LINE( RPAD(v_emp_dept.emp_name, 8) || ' | ' || RPAD(v_emp_dept.dept_name, 8) || ' | ' || v_emp_dept.salary ); END LOOP; CLOSE v_result_cursor; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('异常:' || SQLCODE || ' - ' || SQLERRM); IF v_result_cursor%ISOPEN THEN CLOSE v_result_cursor; END IF; END;

引用游标的优势

  • 类型安全:通过REF CURSOR RETURN明确结果集结构,若FETCH变量与结构不匹配,编译时即报错;
  • 可复用性:在包中定义REF CURSOR类型,多个子程序可复用,减少代码冗余;
  • 可读性强:自定义记录类型(如emp_dept_record)明确列含义,比匿名变量更易维护。
评论
后发表回复

作者

文章

阅读量

获赞

扫一扫
联系客服