zoukankan      html  css  js  c++  java
  • 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;
    /
  • 相关阅读:
    【洛谷3778】[APIO2017] 商旅(分数规划+Floyd)
    【AT4114】[ARC095D] Permutation Tree(简单题)
    【AT4352】[ARC101C] Ribbons on Tree(容斥+DP)
    【AT4169】[ARC100D] Colorful Sequences(DP)
    【洛谷4581】[BJOI2014] 想法(随机算法)
    【洛谷5659】[CSP-S2019] 树上的数(思维)
    【AT4439】[AGC028E] High Elements(线段树)
    【CF590E】Birthday(AC自动机+二分图匹配)
    【洛谷4298】[CTSC2008] 祭祀(Dilworth定理+二分图匹配)
    【洛谷3774】[CTSC2017] 最长上升子序列(杨表)
  • 原文地址:https://www.cnblogs.com/yhq1314/p/10615280.html
Copyright © 2011-2022 走看看