Oracle PL/SQL进阶编程(第十五弹:动态SQL语句)

理解动态SQL语句

动态SQL语句基础

动态SQL语句不仅是指SQL语句是动态拼接而成的,更主要的是SQL语句所使用的对象也是运行时期才创建的。出现这种功能跟PL/SQL本身的早起绑定特性有关,早PL/SQL中,所有的对象必须已经存在于数据库中才能执行,比如要查询emp表,emp表必须已经存在,否则会报错。此时可以通过动态SQL,因为动态SQL不被PL/SQL引擎编译时分析,而是在运行时进行分析并执行。

虽然动态SQL语句可以让我们在运行时动态地切换表名或字段名,以及在PL/SQL中执行DDL语句,但是在如下方面仍然不及静态SQL语句方便:
- 静态SQL在编译或测试时,可以立即知道对错,比如对象是否存在,权限是否具备,而动态SQL要在运行时才知道。
- 使用静态SQL时,可以对要执行的SQL进行性能优化调整,动态SQL不具备这种能力。

动态SQL使用时机

举个例子,我们经常会需要临时存储中间数据,因此会先检测目标表是否存在,如果存在则插入数据,如果不存在则先创建表,再插入数据。
如果我们在PL/SQL代码中直接用CREATE TABLE,会报错,所以必须把CREATE TABLE语句使用动态SQL来执行:
EXECUTE IMMEDIATE 'CREATE TABLE ...';

下面是使用动态SQL的几个时机:
- 由于在PL/SQL中只能执行静态的查询和DML语句,因此如果 要执行DDL语句,必须借助动态SQL。
- 在开发报表或一些复杂的应用程序逻辑时,如果要基于参数化的查询方式,比如动态的表字段和动态的表名称,可以使用动态SQL。
- 基于数据表存储业务规则和软件代码,可以将很多的业务规则的代码写在一个表的记录中,在程序需要时检索不同的业务逻辑的代码动态地执行。

从Oracle 7开始,可以使用DBMS_SQL包来动态执行动态SQL语句,在Oracle 8i之后,Oracle提供了执行动态SQL语句的另外一个选择:本地动态SQL(NDS)。NDS是PL/SQL原生部分,比使用DBMS_SQL更简单更方便,它仅提供了一个名为EXECUTE IMMEDIATE的过程。

本地动态SQL

本地动态SQL缩写为NDS,全称是Native Dynamic SQL。NDS提供了比DBMS_SQL更简单的语法,但是NDS不支持事先不知道参数的个数、名称或数据类型的动态SQL语句,此时需要使用DBMS_SQL来解决。

可以使用如下3种不同类型的动态方法使用本地动态SQL:
- EXECUTE IMMEDIATE:该语句可以处理多数动态SQL操作,包括DDL语句,比如CREATE、ALTER、DROP等;DCL语句,比如GRANT、REVOKE等;DML语句,比如INSERT、UPDATE、DELETE等,以及单行的SELECT语句。不能使用EXECUTE IMMEDIATE来处理多行的查询语句,多行查询需要用OPEN FOR。
- 使用OPEN FOR、FETCH和CLOSE语句执行多行查询。
- 使用批量SQL的处理语句。

使用EXECUTE IMMEDIATE

执行SQL语句和PL/SQL语句块

如下代码动态地创建了一个表,并向表中插入一条数据:

DECLARE
    sql_statement VARCHAR2(100);
    plsql_block VARCHAR2(500);
BEGIN
    sql_statement := 'CREATE TABLE ddl_demo(in NUMBER, amt NUMBER)';
    EXECUTE IMMEDIATE sql_statement;
    sql_statement := 'INSERT INTO ddl_demo VALUES(1, 100)';
    EXECUTE IMMEDIATE sql_statement;

    plsql_block := 
        'DECLARE
            i INTEGER := 10;
            FOR j IN 1.. i LOOP
                INSERT INTO ddl_demo VALUES(j, j * 100);
            END LOOP;
        END;'
    EXECUTE IMMEDIATE plsql_block;

要注意,使用EXECUTE IMMEDIATE执行一个SQL语句时,不要在语句后面放分号,只有在执行PL/SQL语句块时才需要添加分号。

使用绑定变量

代码如下 :

DECLARE
    v_loc VARCHAR2(20) := '南京';
    v_deptno := NUMBER(2) := 30;
    sql_stmt VARCHAR2(100);
BEGIN
    sql_stmt := 'UPDATE dept SET loc = :1 WHERE deptno = :2';
    EXECUTE IMMEDIATE sql_stmt USING v_loc, v_deptno;
END;

在SQL语句中使用绑定变量时,仅能对用于数据值的表达式进行替换,比如静态文字、变量或复杂表达式,而不能对方案元素使用绑定 变量,比如将表名和列名作绑定表达式,或者是对整个SQL语句块使用绑定表达式,比如一个WHERE子句。如果要动态定义方案元素,需要使用字符串拼接的方式对字符串进行拼接。

使用RETURNING INTO子句

RETURNING INTO只能处理单行的DML语句,如果DML语句作用在多行上,则必须要使用BULK子句。

DECLARE
   v_empno NUMBER(4) := 7369;
   v_percent NUMBER(4, 2) := 0.12;
   v_salary  NUMBER(10, 2);
   sql_stmt  VARCHAR2(500);
BEGIN
    sql_stmt := 'UPDATE emp SET sal = sal * (1 + :percent) '
             || ' WHERE empno = :empno RETURNING al INTO :salary';
    EXECUTE IMMEDIATE sql_stmt USING v_percent, v_empno RETURNING INTO v_salary;
    DBMS_OUTPUT.put_line('调整后的工资为:' || v_salary);
END;

执行单行查询

当使用动态SQL语句执行单行查询时,可以使用EXECUTE IMMEDIATE的INTO子句将查询的额结果字段写到一个或多个绑定变量或记录类型的绑定变量中。

DECLARE
    sql_stmt  VARCHAR2(500);
    v_deptno NUMBER(4) := 20;
    v_empno NUMBER(4) := 7369;
    v_dname VARCHAR2(20);
    v_loc VARCHAR2(20);
    emp_row emp%ROWTYPE;
BEGIN
    sql_stmt := 'SELECT dname, loc FROM dept WHERE deptno = :deptno';
    EXECUTE IMMEDIATE sql_stmt INTO v_dname, v_loc USING v_deptno;

    sql_stmt := 'SELECT * FROM emp WHERE empno = :empno';
    EXECUTE IMMEDIATE sql_stmt INTO emp_row USING v_empno;

多行查询语句

在使用静态SQL处理多行查询时,需要使用游标来遍历循环,动态SQL要查询多行的话,也需要类似的处理。

看代码:

DECLARE
   TYPE emp_cur_type IS REF CURSOR;          --定义游标类型    
   emp_cur emp_cur_type;                     --定义游标变量
   v_deptno NUMBER(4) := '&deptno';          --定义部门编号绑定变量
   v_empno NUMBER(4);
   v_ename VARCHAR2(25);
BEGIN
    OPEN emp_cur FOR                          --打开动态游标
       'SELECT empno, ename FROM emp '||
       'WHERE deptno = :1'
    USING v_deptno;
    LOOP
        FETCH emp_cur INTO v_empno, v_ename;  --循环提取游标数据
        EXIT WHEN emp_cur%NOTFOUND;           --没有数据时退出循环
    END LOOP;
    CLOSE emp_cur;                            --关闭游标变量     
EXCEPTION
    WHEN OTHERS THEN 
        IF emp_cur%FOUND THEN
            CLOSE emp_cur;
        END IF;
END;

使用批量绑定

批量EXECUTE IMMEDIATE

EXECUTE IMMEDIATE使用BULK子句来提供批量绑定的能力,代码如下:

DECLARE
   TYPE ename_table_type IS TABLE OF VARCHAR2(20) INDEX BY BINARY_INTEGER;
   TYPE empno_table_type IS TABLE OF NUMBER(24) INDEX BY BINARY_INTEGER; 
   ename_tab ename_table_type;              --定义保存多行返回值的索引表
   empno_tab empno_table_type;  
   v_deptno NUMBER(4) := '&deptno';          --定义部门编号绑定变量
   sql_stmt VARCHAR2(500);
BEGIN
   --定义多行查询的SQL语句
   sql_stmt:='SELECT empno, ename FROM emp '||'WHERE deptno = :1';
   EXECUTE IMMEDIATE sql_stmt 
   BULK COLLECT INTO empno_tab,ename_tab               --批量插入到索引表
   USING v_deptno;   
   FOR i IN 1..ename_tab.COUNT LOOP                    --输出返回的结果值 
      DBMS_OUTPUT.put_line('员工编号'||empno_tab(i)
                                         ||'员工名称:'||ename_tab(i));
   END LOOP;          
END;

批量OPEN FOR FETCH

OPEN FOR FETCH同样提供了BULK子句来进行处理,代码如下:

DECLARE
   TYPE ename_table_type IS TABLE OF VARCHAR2(20) INDEX BY BINARY_INTEGER;
   TYPE empno_table_type IS TABLE OF NUMBER(24) INDEX BY BINARY_INTEGER;
   TYPE emp_cur_type IS REF CURSOR;         --定义游标类型    
   ename_tab ename_table_type;              --定义保存多行返回值的索引表
   empno_tab empno_table_type;  
   emp_cur emp_cur_type;                    --定义游标变量
   v_deptno NUMBER(4) := '&deptno';         --定义部门编号绑定变量
BEGIN
   OPEN emp_cur FOR                         --打开动态游标
      'SELECT empno, ename FROM emp '||
      'WHERE deptno = :1'
   USING v_deptno;
   FETCH emp_cur BULK COLLECT INTO empno_tab, ename_tab; --批量提取游标数据  
   CLOSE emp_cur;                                        --关闭游标变量
   FOR i IN 1..ename_tab.COUNT LOOP                      --输出返回的结果值 
      DBMS_OUTPUT.put_line('员工编号'||empno_tab(i)
                                         ||'员工名称:'||ename_tab(i));
   END LOOP;       
END;

批量FORALL

之前的批量技术都是用于提取数据,而FORALL允许在EXECUTE IMMEDIATE中批量绑定输入参数,代码如下:

DECLARE
   --定义索引表类型,用来保存从DML语句中返回的结果
   TYPE ename_table_type IS TABLE OF VARCHAR2(25) INDEX BY BINARY_INTEGER;
   TYPE sal_table_type IS TABLE OF NUMBER(10,2) INDEX BY BINARY_INTEGER;   
   TYPE empno_table_type IS TABLE OF NUMBER(4);         --定义嵌套表类型,用于批量输入员工编号  
   ename_tab ename_table_type;
   sal_tab sal_table_type;
   empno_tab empno_table_type;
   v_deptno NUMBER(4) :=20;                             --定义部门绑定变量
   v_percent NUMBER(4,2) := 0.12;                       --定义加薪比率绑定变量
   sql_stmt  VARCHAR2(500);                             --保存SQL语句的变量
BEGIN
   empno_tab:=empno_table_type(7369,7499,7521,7566);    --初始化嵌套表
     --定义更新emp表的sal字段值的动态SQL语句
   sql_stmt:='UPDATE emp SET sal=sal*(1+:percent) '
             ||' WHERE empno=:empno RETURNING ename,sal INTO :ename,:salary';
   FORALL i IN 1..empno_tab.COUNT                        --使用FORALL语句批量输入参数
      EXECUTE IMMEDIATE sql_stmt USING v_percent, empno_tab(i)  --这里使用来自嵌套表的参数
      RETURNING BULK COLLECT INTO ename_tab,sal_tab;   --使用RETURNING BULK COLLECT INTO子句获取返回值
   FOR i IN 1..ename_tab.COUNT LOOP                    --输出返回的结果值 
      DBMS_OUTPUT.put_line('员工'||ename_tab(i)||'调薪后的薪资:'||sal_tab(i));
   END LOOP;
END;

动态SQL的使用建议

用绑定变量改善性能

看如下代码:

EXECUTE IMMEDIATE 'DELETE FROM emp WHERE empno = ' || TO_CAHR(emp_id);
EXECUTE IMMEDIATE 'DELETE FROM emp WHERE empno = :num' USING emp_id;

这两行的爱吗效果是一样的,但是建议优先考虑绑定变量而不是字符串连接,原因如下:
- 绑定比连接具有更高的性能:由于使用绑定变量,不会每次都改变SQL语句,因此可以使用SGA中缓存的预备游标来快速处理SQL语句。
- 绑定变量更容易编写和维护:使用绑定变量不用担心数据类型转换的问题,本地动态SQL引擎可以处理所有关于转换相关的额问题,而对于拼接字符串来说,必须要经常使用TO_CHAR,TO_DATE等函数处理数据类型转换。
- 避免隐式类型转换:连接SQL可能会导致数据库隐式转换,有可能会导致隐式转换为不想要的结果。
- 绑定避免代码注入:使用绑定变量可以避免SQL注入攻击,而连接字符串有可能会导致这种危险的情形。

使用调用者权限

默认情况下,调用动态SQL子程序的用户是使用创建者的权限来执行动态SQL,可以通过在子程序上使用AUTHID子句使得子程序使用调用者权限来执行,这样就不会绑定在一个特定的schema对象上,代码如下:

CREATE OR REPLACE PROCEDURE drop_obj (kind IN VARCHAR2, NAME IN VARCHAR2)
AUTHID CURRENT_USER       --定义调用者权限
AS
BEGIN
   EXECUTE IMMEDIATE 'DROP ' || kind || ' ' || NAME;
EXCEPTION
WHEN OTHERS THEN
   RAISE;   
END;

传递NULL参数

如果要为动态SQL传递NULL值,直接写USING NULL会导致错误,因为USING语句不接受NULL作为传递的参数。为了解决这个问题,可以定义一个未赋值的变量,该变量在未赋值时自动为NULL值,代码如下:

DECLARE
   v_null   CHAR (1);                      --在运行时该变量自动被设置为NULL值
BEGIN
   EXECUTE IMMEDIATE 'UPDATE emp SET comm = :x' USING v_null;  --传入NULL值
END;

动态SQL异常处理

在动态拼接一个SQL时,因为拼写错误或空格问题都会导致语句执行失败,此时Oracle会抛出错误提示,但通常这种错误信息很不全面,初遇应用程序健壮性的考虑,提供以下几个建议:
- 总是在调用EXECUTE IMMEDIATE和OPEN FOR语句的地方包含异常处理块。
- 在每一个异常处理块中记录错误消息和执行的SQL语句,以便发现错误。
- 可以使用DBMS_OUTPUT包添加一个追踪机制以便能更好地发现错误。

代码如下:

CREATE OR REPLACE PROCEDURE ddl_execution (ddl_string IN VARCHAR2)
   AUTHID CURRENT_USER IS            --使用调用者权限
BEGIN
   EXECUTE IMMEDIATE ddl_string;     --执行动态SQL语句
EXCEPTION
   WHEN OTHERS                       --捕捉错误  
   THEN
      DBMS_OUTPUT.PUT_LINE (      --显示错误消息
         '动态SQL语句错误:' || DBMS_UTILITY.FORMAT_ERROR_STACK);
      DBMS_OUTPUT.PUT_LINE (      --显示当前执行的SQL语句
         '执行的SQL语句为: "' || ddl_string || '"');
      RAISE;
END ddl_execution;

猜你喜欢

转载自blog.csdn.net/lianjiww/article/details/81586799