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

--什么是动态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;
/

 

posted @ 2019-03-28 15:25  春困秋乏夏打盹  阅读(1214)  评论(0编辑  收藏  举报