PL/SQL 训练12--动态sql和绑定变量

时间:2022-11-04 11:14:03

--什么是动态SQL?动态PL/SQL
--动态SQL是指在运行时刻才构建执行的SQL语句
--动态PL/SQL是指整个PL/SQL代码块都是动态构建,然后再编译执行

--动态SQL来可以用来干什么?

--执行DDL语句

--支持WEB引用的即席查询和即席更新需求

--软编码的业务规则和公式

--先来看dbms_sql包的使用
DECLARE v_cur number;
v_sql varchar2(1000);
v_result number;
BEGIN
v_cur := dbms_sql.open_cursor;
v_sql := 'update ma_users set user_point = :point where user_name = :name';
dbms_sql.parse(v_cur, v_sql, dbms_sql.native);
dbms_sql.bind_variable(v_cur, ':point', 10000);
dbms_sql.bind_variable(v_cur, ':name', '乱世佳人');
v_result := dbms_sql.execute(v_cur);
dbms_sql.close_cursor(v_cur); END;
/

--NDS:原生动态SQL。相较于DBMS_SQL包执行动态SQL要简单的多

--怎么简单法呢?

--只要一个语句就够了
EXECUTE IMMEDIATE --立即执行

--语法
EXECUTE IMMEDIATE SQL_string
[into {defined_varibale[,defined_varibale2]...|record} ]
[using [in|out|in out]] bind_argument
[, [in|out|in out] bind_argument];

--sql_string :包含了SQL语句或者PL/SQL代码块的字符串表达式
--defined_varibale:用于接收查询中某一列值的变量
--record: 用户自定义类型或者基于%rowtype的记录,可以接收查询返回的一行值
--bind_argument:表达式,表达式的值将传给SQL语句或者PL/SQL块,也可以是一个标识符
--这个标识符作为PL/SQL块中调用的函数或者过程的输入或者输出变量

--into:这个字句用于单行的查询,对于查询结果的每一列的值,必须提供一个单独的变量或者一个兼容的记录类型的一个
--字段
--USING子句:利用这个子句给SQL字符串提供绑定参数,同时用于动态SQL和动态PL/SQL
--使用动态PL/SQL时可以指定一个参数模式,缺省模式是IN

DECLARE

  v_cur    number;
v_sql varchar2(1000);
v_result number;
BEGIN v_sql := 'update ma_users set user_point = :point where user_name = :name';
execute immediate v_sql using 10000, '乱世佳人'; END;
/ --EXECUTE IMMEDIATE 可以用于除多行查询以外的SQL语句或者PL/SQL块 ,bulk collect INTO
--如果sql_string后面带分号,则会按照一个PL/SQL块来处理 否则就是DML或者DDL
--字符串可以带有绑定参数的占位符,但是对象的名字,比如表的名字或者列的名字,不能通过绑定变量传进去
DECLARE v_cur number;
v_sql varchar2(1000);
v_result number;
BEGIN v_sql := 'BEGIN update :TAB_NAME set user_point = :point where user_name = :name; END ;';
execute immediate v_sql
using 'MA_USERS',10000, '乱世佳人'; END;
/

--想一想为什么对象名字不能通过绑定变量进行传递?

--例子,
--最简单的例子,执行建表语句
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE USER_ORDER(USER_ID VARCHAR2(32),ORDER_ID VARCHAR2(32))';
END;
/
--更简单,可以创建通用的方法,比如
create or replace procedure exec_ddl(ddl_string in varchar2)
authid current_user
is
begin
EXECUTE IMMEDIATE ddl_string;
end ;
/
begin
exec_ddl('create table user_order_product(order_id varchar2(32),product_id varchar2(32))');
end ;
/ --传递表名,返回数量
create or replace function count_tab(i_table_name in varchar2)
return number is
v_count number;
begin EXECUTE IMMEDIATE 'select count(1) from ' || i_table_name
into v_count;
-- EXECUTE IMMEDIATE 'select count(1) from :table_name'
-- into v_count using i_table_name;
return v_count;
end;
/ begin
if count_tab('ma_users') <10 then
dbms_output.put_line('用户量不超过十个,太逊了');
end if;
end ;
/

--动态更新列,传进列的名称,就可以更新相应列的值

create or replace function update_col(i_col        in varchar2,
i_val in varchar2,
i_start_date in date,
i_end_date in date) return number is begin
execute immediate 'update ma_users set ' || i_col ||
'=:1 where created_date between :2 and :3'
using i_val, i_start_date, i_end_date;
return sql%rowcount;
end;
/

--上述例子使用了绑定参数,对UPDAE语句分析结束后,引擎就会把几个占位符用USING子句中的值替换

--当一个语句在执行的时候,运行引擎会把SQL语句中的每一个占位符用USING语句中对应的绑定参数替换
--注意不能传进NULL直接量,必须通过一个数据类型正确但恰好是NULL值的变量传入

--using语句不能绑定专属于PL/SQL的数据类型,比如布尔类型,关联数组以及用户自定义的记录类型
--支持所有的SQL数据类型

--现在假设需要设计一个任务调度,可以每隔一段时间跑一次,需要入参可配置,任务也可配置

create table ma_schedue_task(task_id varchar2(50),procedure_name varchar2(100));
create table ma_schedue_param(task_id varchar2(50),param_order number,param_value varchar2(100));
declare
v_task_id varchar(32);
begin
insert into ma_schedue_task
values(sys_guid(), 'test_bind(:1,:2)')
returning task_id into v_task_id;
insert into ma_schedue_param values (v_task_id, 1, 'test1');
insert into ma_schedue_param values (v_task_id, 2, 'test2');
commit;
end;
/
create or replace procedure test_bind(i_test1 in varchar2,
i_test2 in varchar2) is begin
dbms_output.put_line(i_test1 || i_test2); end test_bind;
/
declare
cursor cur_task is
select * from ma_schedue_task;
v_sql varchar2(4000);
begin for v in cur_task loop
v_sql := ' begin execute immediate '' begin ' || v.procedure_name ||
' ; end;'' using ';
for param in (select r.param_value
from ma_schedue_param r
where r.task_id = v.task_id
order by param_order) loop
v_sql := v_sql || ' ''' || param.param_value || ''',';
end loop;
v_sql := rtrim(v_sql, ',') || '; end;'; dbms_output.put_line(v_sql);
execute immediate v_sql;
end loop;
end;
/

---OPEN FOR 语句
--上节课讲到游标变量时,用到这个语句
--这个语句可以用来实现多行的动态查询

--语法
OPEN {cursor_variable|:host_cursor_variable} for sql_string
[using bind_argument[,bind_argument]... ]; --cursor_variable; 弱类型的游标变量
-- :host_cursor_variable :在PL/SQL宿主环境比如OCI程序中声明的游标变量
-- sql_string: 包含了将要动态执行的SELECT 语句
--Using 字句:跟 execute immediate语句遵守同样的规则 create or replace function show_table(i_table in varchar2,
i_where in varchar2)
return sys_refcursor is
v_cur sys_refcursor;
begin open v_cur for 'select * from ' || i_table || ' where ' || i_where;
return v_cur;
end show_table;
/
declare
v_cur sys_refcursor;
v_user ma_users%rowtype;
begin
v_cur := show_table('ma_users', ' user_name=''乱世佳人''');
loop
fetch v_cur
into v_user;
exit when v_cur%notfound;
dbms_output.put_line(v_user.user_name || '积分' || v_user.user_point); end loop;
close v_cur;
end;
/

--一旦使用OPEN FOR 打开一个查询,接下来获取数据,关闭游标变量,检查游标属性的语法规则和静态游标变量
--以及硬编码的显示游标都是一样的

--执行一个OPEN FOR 语句是,PL/SQL引擎将会做以下事情

--用一个游标变量关联查询字符串中的查询语句
--对绑定参数值,然后用这些值替换查询字符串中的占位符
--执行查询
--识别出结果集
--将游标位置置于结果集的第一行
--把已处理行计数器归零,这个计数器也就是SQL/rowcount返回的值

--值得注意的是,查询语句中的任何绑定参数,都是游标变量在打开时才求值的
--也就是说,如果要把不同的绑定参数值用于同一个动态查询,必须用这些参数再执行一个新的OPEN FOR语句

create or replace procedure show_col(i_table in varchar2,
i_col in varchar2,
i_where in varchar2 := null) is
v_cur sys_refcursor;
v_val varchar2(1000);
begin
open v_cur for 'select ' || i_col || ' from ' || i_table || ' where ' || nvl(i_where,
' 1=1');
loop
fetch v_cur
into v_val;
exit when v_cur%notfound; if v_cur%rowcount = 1 then
dbms_output.put_line(i_col || ' of ' || i_table);
end if;
dbms_output.put_line(v_val);
end loop;
close v_cur;
end show_col;
/
begin
show_col('ma_users','user_name');
end ;
/
--上述例子把数据提取到一个单独的变量中,还可以把数据提取到一系列变量中
--也可以放到一个记录中
--可以为不同的需求创建不同类型的记录类型,这些类型可以放置包中,以便全局可用

--OPEN FOR 中的USING子句
--对于查询语句只能用IN 模式
--通过绑定参数,可以提升SQL语句性能,而且代码更容易编写和维护
--可以显著减少需要缓存在SGA中的编译后不同的语句数量

create or replace procedure show_col1(i_table    in varchar2,
i_col in varchar2,
i_dcol in varchar2,
i_bg_date in date,
i_end_date in date) is
v_cur sys_refcursor;
v_val varchar2(1000);
begin
open v_cur for 'select ' || i_col || ' from ' || i_table || ' where ' || i_dcol || ' between :1 and :2'
using i_bg_date, i_end_date;
loop
fetch v_cur
into v_val;
exit when v_cur%notfound; if v_cur%rowcount = 1 then
dbms_output.put_line(i_col || ' of ' || i_table);
end if;
dbms_output.put_line(v_val);
end loop;
close v_cur;
end show_col1;
/ begin
show_col1('ma_users','user_name','REGISTER_DATE',date '2016-01-01',sysdate);
end ;
/

--四种动态SQL方法
-----------------------------------------------------------------------------------------------------------
--类型            说明                                       使用NDS语句
--第一种方法        非查询;只用于UPATE,INSERT,MERGE,DELETE和DDL语句且不带有绑定变量      不带USING into子句的
                                                    -- EXECUTE IMMEDIATE语句
--第二种方法        非查询;只用于UPATE,INSERT,MERGE,DELETE且带有绑定变量个数固定        带有USING子句的
                                                    -- EXECUTE IMMEDIATE语句
--第三种方法        带有确定数量的列和绑定变量,返回只有一行数据                    带有USING into子句的
                                                    --之单行查询 EXECUTE IMMEDIATE语句
--第三种方法之      带有固定数量的列和绑定变量,返回多行数据                      带有USING BULK COLLECT INTO 的EXECUTE IMMEDIATE语句
                                                      --多行查询 或者动态字符串的OPEN FOR 语句
--第四种方法      语句中选定的列的数量或者绑定数量等到允许时刻才能确定                  使用DBMS_SQL包
--
-------------------------------------------------------------------------------------------------------------

--绑定变量
--使用绑定变量的规则和情况

--SQL语句中可以绑定的是,可以把动态字符串中的占位符替换成数值的直接量(文本,变量,复杂的表达式)
--不能绑定模式元素的名字(表或者列)或者SQL语句的一整块,比如where 子句
--对于这部分,必须使用拼接的方式

begin
execute immediate 'update :table set :col = :value where 1=1'
using 'ma_users', 'user_point', 10000;
end;
/
--为什么会有一个限制呢?
--当给EXECUTE IMMEDIATE 传入一个字符串时,运行时引擎首先必须要解析这个语句
--解析的目的是保证SQL语句是定义良好的
begin
execute immediate 'update ma_users set user_point = :value where 1=1'
using 10000;
end;
/

--参数的模式:in,out,in out

--执行动态查询时,所有的绑定参数都必须是IN 模式,除非使用了RETURNING
DECLARE
V_POINT NUMBER := 1000;
V_NAME VARCHAR2(20) := '乱世佳人';
V_PHONE varchar2(50);
begin
execute immediate 'update ma_users set user_point = :value where USER_NAME= :NAME
RETURNING USER_PHONE INTO :PHONE'
using V_POINT, V_NAME, OUT V_PHONE;
dbms_output.put_line(v_phone);
end;
/
--除了能用在RETURNING子句,OUT,IN OUT模式的绑定参数在执行动态PL/SQL时发挥比较大的作用
--在动态PL/SQL中绑定参数的模式必须要和PL/SQL程序中参数模式一致 --重复的占位符
--NDS根据位置而不是名字把USING语句的绑定参数关联到占位符的
--当执行一个动态SQL字符串,必须为每一个占位符提供一个参数,即便这些占位符是重复的
--如果执行的是一个动态PL/SQL块,必须为每一个唯一占位符提供一个参数
DECLARE
V_POINT NUMBER := 1000;
V_NAME VARCHAR2(20) := '乱世佳人';
V_PHONE varchar2(50);
V_email varchar2(50);
begin
execute immediate 'update ma_users set user_point = :value where USER_NAME= :value
RETURNING USER_PHONE,user_email INTO :1,:2'
using V_POINT, V_NAME, OUT V_PHONE, out V_email;
dbms_output.put_line(v_phone || V_email);
end;
/ DECLARE
V_POINT NUMBER := 1000;
V_NAME VARCHAR2(20) := '乱世佳人';
V_PHONE varchar2(50);
V_email varchar2(50);
begin
execute immediate 'begin update ma_users set user_name = :value where USER_NAME= :value
RETURNING USER_PHONE,user_email INTO :1,:2 ; end ;'
using V_NAME, OUT V_PHONE,out V_email;
dbms_output.put_line(v_phone||v_email);
end;
/
--NULL值的传递
--把NULL值隐藏在一个变量后面
--通过转换函数把NULL值显示的转换为一个有类型的值 DECLARE
V_POINT NUMBER := null;
V_NAME VARCHAR2(20) := '乱世佳人';
V_PHONE varchar2(50);
begin /*execute immediate 'update ma_users set user_point = :value where USER_NAME= :NAME
RETURNING USER_PHONE INTO :PHONE'
using null, V_NAME, OUT V_PHONE;
dbms_output.put_line(v_phone);*/ /*execute immediate 'update ma_users set user_point = :value where USER_NAME= :NAME
RETURNING USER_PHONE INTO :PHONE'
using V_POINT, V_NAME, OUT V_PHONE;
dbms_output.put_line(v_phone);
*/ execute immediate 'update ma_users set user_point = :value where USER_NAME= :NAME
RETURNING USER_PHONE INTO :PHONE'
using to_number(null), V_NAME, OUT V_PHONE;
dbms_output.put_line(v_phone);
end;
/

---动态PL/SQL
--NDS可以为我们做以下事情
--创建一个程序,包括带有全局访问的数据结构的包
--通过名字获得或修改全局变量的值
--调用那些在编译时刻还不知道名字的函数或者过程

--使用动态PL/SQL块和NDS的规则和技巧

--动态字符串必须是一个有效的PL/SQL块,这个块必须以DECLARE或者BEGIN关键字开始
--使用END关键字和分号结束。如果字符串不以分号结尾,是不会被识别成PL/SQL块的

--在动态块中,只能访问属于全局范围的PL/SQL代码元素。动态块是在局部包围块的作用范围之外执行的
--在动态PL/SQL块中抛出的错误可以在运行EXECUTE IMMEDIATE语句的局部块中捕获并处理

create or replace procedure do_plsql(i_plsql in varchar2)
is
begin
execute immediate 'begin '|| rtrim(i_plsql,';')||' ; end ;';
end ;
/
--下面这个例子反映了规则二
declare
num number ;
begin
do_plsql('num := 5');
exception
when others then
dbms_output.put_line(sqlerrm);
end ;
/ begin
<<test>>
declare
num number;
begin
do_plsql('test.num := 5');
end;
end;
/
create or replace package pkgvars
is
num number ;
end pkgvars; declare
num number ;
begin
do_plsql('pkgvars.num := 5;');
end ;
/

--用动态块替换重复的代码

--比如有以下方法
procedure do_task(task_name in varchar2) is begin
if task_name = 'test' then
test;
elsif task_name = 'test2' then
test2;
......
end if;
end ;
/
procedure do_task(task_name in varchar2) is begin
execute immediate
'begin '||task_name||' ; end ;';
end ;
/

--NDS的建议

--对于共享的程序使用调用者权限
create or replace procedure exec_ddl(ddl_string in varchar2)
authid current_user
is
begin
EXECUTE IMMEDIATE ddl_string;
end ;
/ --预估并处理动态的错误
--如果使用大量的动态SQL,很容易迷失方向以至于在代码调试上浪费大量时间
--在调用EXECUTE IMMEDIATE和OPEN FOR 时,总是带上一个异常处理单元
--在每一个异常处理句柄,记录下显示错误发生时的错误信息以及SQL语句
--可以考虑在这些语句之前加上一个“跟踪机制”
create or replace procedure exec_ddl(ddl_string in varchar2)
authid current_user
is
begin
EXECUTE IMMEDIATE ddl_string;
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.put_line('FAILLURE:'||DBMS_UTILITY.format_error_backtrace);
DBMS_OUTPUT.put_line('ON :'||ddl_string);
end ;
/ --使用绑定而不是拼接
DECLARE
V_POINT NUMBER := 1000;
V_NAME VARCHAR2(20) := '乱世佳人';
V_PHONE varchar2(50);
begin
execute immediate 'update ma_users set user_point = :value where USER_NAME= :NAME'
using V_POINT, V_NAME
dbms_output.put_line(v_phone);
end;
/
DECLARE
V_POINT NUMBER := 1000;
V_NAME VARCHAR2(20) := '乱世佳人';
V_PHONE varchar2(50);
begin
execute immediate 'update ma_users set user_point =' || V_POINT ||
'where USER_NAME=''' || V_NAME || '''';
dbms_output.put_line(SQL%ROWCOUNT);
end;
/
--只要有可能使用绑定的方式就不要依赖与拼接

--绑定通常更快速
--绑定的编写和维护都很容易
--绑定有助于避免隐式转换
--绑定避免了发生代码注入的可能性

--对于一些场景,如果使用拼接更有效,那也要毫不犹豫的使用拼接方式

--把代码注入的风险最小化
create or replace procedure show_table1(i_table in varchar2,
i_where in varchar2) is
v_sql varchar2(1000);
begin
v_sql := 'declare v_row ' || i_table || '%rowtype;
begin
select * into v_row from ' || i_table || ' where ' || i_where || '
end ;';
dbms_output.put_line(v_sql);
-- execute immediate v_sql;
end show_table1;
/ declare
begin
show_table1('ma_users',
' user_name=''乱世佳人'';delete from ma_users ; ');
end;
/ --代码注入也叫SQL注入,可以严重的威胁程序的安全,动态PL/SQL块的执行为代码注入开启了最大的可能性
--限制用户权限
--尽可能使用绑定变量,但使用绑定,也丧失了一些灵活性
--检测动态文本中的危险文本
--用DBMS_ASSERT检验输入
DBMS_ASSERT.SIMPLE_SQL_NAME ---什么时候使用DBMS_SQL
---解析非常长的字符串
--EXECUTE IMMEDIATE 执行的字符串大小限制32K,在11g中可以处理一个CLOB,最大长度4GB
dbms_sql.parse --可以解析任意长度的SQL和PLSQL ---得到查询的列的信息
--dbms_sql可以对动态游标中的列进行描述,以记录的关联数组的形式返回每个列的信息
--用这个功能,可以写出非常通用的游标处理代码
--动态SQL的第四种方法
declare
cur pls_integer := dbms_sql.open_cursor;
cols dbms_sql.desc_tab;
ncols pls_integer;
begin
dbms_sql.parse(cur,'select user_name ,user_point from ma_users',dbms_sql.native);
dbms_sql.describe_columns(cur,ncols,cols);
for i in 1..ncols loop
dbms_output.put_line(cols(i).col_name);
end loop ;
dbms_sql.close_cursor(cur);
end ;
/
--实现第四种方法的动态SQL的需求
declare
cursor cur_task is
select * from ma_schedue_task;
v_sql varchar2(4000);
v_cur number;
v_result number;
begin for v in cur_task loop
v_sql := ' begin ' || v.procedure_name || ';end;';
v_cur := dbms_sql.open_cursor;
dbms_sql.parse(v_cur, v_sql, dbms_sql.native);
for param in (select r.param_order, r.param_value
from ma_schedue_param r
where r.task_id = v.task_id
order by param_order) loop
dbms_sql.bind_variable(v_cur,
':' || param.param_order,
param.param_value);
end loop;
v_result := dbms_sql.execute(v_cur);
dbms_sql.close_cursor(v_cur);
end loop;
end;
/

--11g新特性

dbms_sql.to_refcursor --原生态动态sql和DBMS_SQL的交互

declare
type string_t is table of varchar2(100); function get_data(i_where in varchar2, i_value in string_t)
return sys_refcursor is
v_sql varchar2(1000);
v_cur number;
v_result number;
v_result_cur sys_refcursor;
begin
v_sql := 'select * from ma_users where ' || i_where;
v_cur := dbms_sql.open_cursor;
dbms_sql.parse(v_cur, v_sql, dbms_sql.native);
for i in 1 .. i_value.count loop
dbms_sql.bind_variable(v_cur, ':' || i, i_value(i));
end loop;
v_result := dbms_sql.execute(v_cur);
v_result_cur := dbms_sql.to_refcursor(v_cur);
return v_result_cur;
end get_data;
begin
declare
v_cur sys_refcursor;
v_row ma_users%rowtype;
begin
v_cur := get_data(' user_name=:1', string_t('乱世佳人'));
loop
fetch v_cur
into v_row;
exit when v_cur%notfound;
dbms_output.put_line(v_row.user_point);
end loop;
close v_cur;
end;
end;
/