Oracle--存储过程

 开发者博客www.developsearch.com

存储过程就是已经编译好的、优化过的放在数据库服务器中的一些SQL语句,可以通过java代码传参数直接调用

 

  • 使用存储过程有以下几个优点:

1、执行速度比普通的SQL语句快

      再运行存储过程前,数据库已对其进行了语法和句法分析,并给出了优化执行方案。这种已经编译好的过程可极大地改善SQL语句的性能。由于执行SQL语句的大部分工作已经完成,所以存储过程能以极快的速度执行。

2、便于集中控制

      当企业规则变化时,只需要在数据库的服务器中修改相应的存储过程,而不需要逐个的在应用程序中修改,应用程序保持不变即可,这样就省去了修改应用程序工作量。

3、可以降低网络的通信量

4、保证数据库的安全性和完整性

      通过存储过程不仅可以使没有权限的用户在控制之下间接地存取数据库,保证数据的安全;而且可以使相关的动作在一起发生,从而可以维护数据库的完整性。

5、灵活性

      存储过程可以用流控制语句编写,具有很强的灵活性,可以完成复杂的判断和运算,可以根据条件执行不通SQL语句。

 

  • 什么时候用到存储过程

1.当一个事务涉及到多个SQL语句时

2.涉及到对多个表的操作时存储过程只在创造时进行编译,以后每次执行存储过程都不需再重新编译避免了执行多个SQL时会多次连接数据库

 

  • 存储过程的语法:

 

CREATE PROC [ EDURE ] procedure_name [ ; number ]

    [ { @parameter data_type }

        [ VARYING ] [ = default ] [ OUTPUT ]

    ] [ ,...n ]

[ WITH

    { RECOMPILE | ENCRYPTION | RECOMPILE , ENCRYPTION } ]

[ FOR REPLICATION ]

 

AS sql_statement [ ...n ]

 

****其中:[] 表示可选

 

参数解释:

procedure_name

 

新存储过程的名称。过程名必须符合标识符规则,且对于数据库及其所有者必须唯一。有关更多信息,请参见使用标识符。

 

要创建局部临时过程,可以在 procedure_name 前面加一个编号符 (#procedure_name),要创建全局临时过程,可以在 procedure_name 前面加两个编号符 (##procedure_name)。完整的名称(包括 # ##)不能超过 128 个字符。指定过程所有者的名称是可选的。

 

;number

 

是可选的整数,用来对同名的过程分组,以便用一条 DROP PROCEDURE 语句即可将同组的过程一起除去。例如,名为 orders 的应用程序使用的过程可以命名为 orderproc;1orderproc;2 等。DROP PROCEDURE orderproc 语句将除去整个组。如果名称中包含定界标识符,则数字不应包含在标识符中,只应在 procedure_name 前后使用适当的定界符。

 

@parameter

 

过程中的参数。在 CREATE PROCEDURE 语句中可以声明一个或多个参数。用户必须在执行过程时提供每个所声明参数的值(除非定义了该参数的默认值)。存储过程最多可以有 2.100 个参数。

 

使用 @ 符号作为第一个字符来指定参数名称。参数名称必须符合标识符的规则。每个过程的参数仅用于该过程本身;相同的参数名称可以用在其它过程中。默认情况下,参数只能代替常量,而不能用于代替表名、列名或其它数据库对象的名称。有关更多信息,请参见 EXECUTE

 

data_type

 

参数的数据类型。所有数据类型(包括 textntext image)均可以用作存储过程的参数。不过,cursor 数据类型只能用于 OUTPUT 参数。如果指定的数据类型为 cursor,也必须同时指定 VARYING OUTPUT 关键字。有关 SQL Server 提供的数据类型及其语法的更多信息,请参见数据类型。

 

 

 

说明  对于可以是 cursor 数据类型的输出参数,没有最大数目的限制。

 

 

VARYING

 

指定作为输出参数支持的结果集(由存储过程动态构造,内容可以变化)。仅适用于游标参数。

 

default

 

参数的默认值。如果定义了默认值,不必指定该参数的值即可执行过程。默认值必须是常量或 NULL。如果过程将对该参数使用 LIKE 关键字,那么默认值中可以包含通配符(%_[] [^])。

 

OUTPUT

 

表明参数是返回参数。该选项的值可以返回给 EXEC[UTE]。使用 OUTPUT 参数可将信息返回给调用过程。Textntext image 参数可用作 OUTPUT 参数。使用 OUTPUT 关键字的输出参数可以是游标占位符。

 

n

 

表示最多可以指定 2.100 个参数的占位符。

 

{RECOMPILE | ENCRYPTION | RECOMPILE, ENCRYPTION}

 

RECOMPILE 表明 SQL Server 不会缓存该过程的计划,该过程将在运行时重新编译。在使用非典型值或临时值而不希望覆盖缓存在内存中的执行计划时,请使用 RECOMPILE 选项。

 

ENCRYPTION 表示 SQL Server 加密 syscomments 表中包含 CREATE PROCEDURE 语句文本的条目。使用 ENCRYPTION 可防止将过程作为 SQL Server 复制的一部分发布。

 

说明  在升级过程中,SQL Server 利用存储在 syscomments 中的加密注释来重新创建加密过程。

 

FOR REPLICATION

 

指定不能在订阅服务器上执行为复制创建的存储过程。.使用 FOR REPLICATION 选项创建的存储过程可用作存储过程筛选,且只能在复制过程中执行。本选项不能和 WITH RECOMPILE 选项一起使用。

 

AS

 

指定过程要执行的操作。

 

sql_statement

 

过程中要包含的任意数目和类型的 Transact-SQL 语句。但有一些限制。

 

n

 

是表示此过程可以包含多条 Transact-SQL 语句的占位符。

 

/*

创建存储过程_插入材料信息返回ID

cursor_define.weavercursor是定义好的公共游标

         MATERIAL_INFO_ID.Currval为插入前生成的sequence

*/

create or replace procedure MATERIAL_INFO_INSERT(name         varchar2,

                                                 type         varchar2,

                                                 spec         varchar2,

                                                 physicalUnit varchar2,

                                                 orderNum     varchar2,

                                                 createTime   varchar2,

                                                 createId     varchar2,

                                                 flag         out integer,

                                                 msg          out varchar2,

                                                 thecursor    IN OUT cursor_define.weavercursor) is

begin

  insert into sy_material_info

    (NAME, TYPE, SPEC, PHYSICAL_UNIT, ORDER_NUM, CREATE_TIME, CREATE_ID)

  values

    (name, type, spec, physicalUnit, orderNum, createTime, createId);

  open thecursor for

    select MATERIAL_INFO_ID.Currval from dual;

end;

 

/*

调用存储过程_插入材料信息返回ID

materialInfoInsertPara为参数

*/

 

char separator = Util.getSeparator() ;

String materialInfoInsertPara = "";

    materialInfoInsertPara = name ;

    materialInfoInsertPara += separator+type;

    materialInfoInsertPara += separator+spec;

    materialInfoInsertPara += separator+physicalUnit;

    materialInfoInsertPara += separator+orderNum;

    materialInfoInsertPara += separator+createTime;

    materialInfoInsertPara += separator+createId;

RecordSet.executeProc("MATERIAL_INFO_INSERT",materialInfoInsertPara);

RecordSet.next();

String materialId = RecordSet.getString(1);

System.out.println("materialId:"+materialId);

 

 

 

 

/*

创建存储过程_新增材料的同时循环插入各分公司的建筑材料价格基础信息

cursor_define.weavercursor是定义好的公共游标

*/

create or replace procedure MATERIAL_INFO_INSERT(name         varchar2,

                                                 type         varchar2,

                                                 spec         varchar2,

                                                 physicalUnit varchar2,

                                                 orderNum     varchar2,

                                                 createTime   varchar2,

                                                 createId     varchar2,

                                                 flag         out integer,

                                                 msg          out varchar2,

                                                 thecursor    IN OUT

                                                

                                                     cursor_define.weavercursor) is/*定义好的公共游标*/

 

  row sy_material_price_info%rowtype; /*定义变量*/

  cursor rows is

    select * from hrmsubcompany;/*查询所有分公司信息*/

 

begin

 

  /*往sy_material_info表中插入材料基本信息*/

  insert into sy_material_info

    (NAME, TYPE, SPEC, PHYSICAL_UNIT, ORDER_NUM, CREATE_TIME, CREATE_ID)

  values

    (name, type, spec, physicalUnit, orderNum, createTime, createId);

 

  /*返回当前sequenceid(主键ID值)*/

  open thecursor for

    select MATERIAL_INFO_ID.Currval from dual;

 

  /*往sy_material_price_info表中插入各分公司的建筑材料价格基础信息*/

  for row in rows loop

    insert into sy_material_price_info

      (MATERIAL_ID,

       COMPANY_ID,

       PRICE,

       BRAND,

       SALES_UNIT,

       TEL,

       UPDATE_TIME,

       UPDATE_ID)

    VALUES

      (MATERIAL_INFO_ID.Currval,

       row.id,

       '',

       '',

       '',

       '',

       createTime,

       createId);

  end loop;

 

end;

 

 

 

/*

临时数据上报利用过程自动创建模板数据表、sequence和触发器

必须要先赋权才能起作用

grant create table to ecology;

grant create sequence to ecology;

grant create any trigger to ecology;

*/

create or replace procedure DATA_TEMPLATE_TABLE_INSERT

(

       tablename     in       varchar2,

       flag             out integer,

       msg              out varchar2,

       thecursor        IN OUT cursor_define.weavercursor

) is

                                            

v_string_table varchar2(1000);    

v_string_sequence varchar2(1000);

v_string_trigger varchar2(1000);

                   

begin

        -- Create table

        v_string_table := 'create table ' || tablename ||' ('

                      ||'ID INTEGER not null primary key,'

                      ||'START_ID INTEGER,'

                      ||'EXECUTOR INTEGER,'

                      ||'DESCRIPTION  VARCHAR2(2000),'

                      ||'UPDATE_TIME   VARCHAR2(100),'

                      ||'UPDATE_ID     INTEGER,'

                      ||'CREATE_TIME   VARCHAR2(100),'

                      ||'CREATE_ID     INTEGER '

                ||')';

        EXECUTE IMMEDIATE v_string_table;

       

        -- Create sequence

        v_string_sequence := 'create sequence ' || tablename || '_ID increment by 1 start with 1 nomaxvalue minvalue 1';

        EXECUTE IMMEDIATE v_string_sequence;

       

        -- Create trigger

        v_string_trigger := 'CREATE OR REPLACE TRIGGER '||tablename||'_Trigger '

                      ||'before insert on '||tablename||' for '

                      ||'each row begin select '||tablename||'_ID.nextval '

                      ||'into :new.id from dual; end; ';

        EXECUTE IMMEDIATE v_string_trigger;

 

        commit;

 

end;

 

/*

给模板数据表动态添加字段

必须要先赋权才能起作用

GRANT ALTER ANY table TO ecology;

*/

 

create or replace procedure data_template_table_alter

(

       TABLE_NAME       in       varchar2,

       FIELD_NAME       in       varchar2,

       FIELD_TYPE  in       varchar2,

       flag             out integer,

       msg              out varchar2,

       thecursor        IN OUT cursor_define.weavercursor

) is

 

v_string_alter varchar2(1000);

 

begin

        -- alter table

        v_string_alter := 'alter table '||TABLE_NAME||' add('||FIELD_NAME||' '||FIELD_TYPE||' )';

        EXECUTE IMMEDIATE v_string_alter;

 

        commit;

 

end;

 

 

 

 

 

 

 

 

savepoint
保存点,是事务中的一点,通过rollback可以返回到某个保存点。
一个事务中可以有多个保存点,一旦事务提交,该事务中的保存点会自动被删除,那么无论刚才做了多少个保存点,都统统没有了。
SAVEPOINT ROLLBACK_C2_POINT;
...
ROLLBACK TO ROLLBACK_C2_POINT;
 

输出异常信息

dbms_output.put_line("..................");

raise_application_error
用于在plsql使用程序中自定义不正确消息,将应用程序专有的错误从服务器端转达到客户端应用程序(其他机器上的SQLPLUS或者其他前台开发语言)。
该异常只好在数据库端的子程序(流程、函数、包、触发器)中运用,而无法在匿名块和客户端的子程序中运用。
语法为 :raise_application_error(error_number,error_msg[,[truefalse]]);
其中
error_number用于定义不正确号,该不正确号必须在-20000到-20999之间的负整数;
error_msg用于指定不正确消息,并且该消息的长度无法超过2048字节;
第三个参数假如为true,则该不正确会被放在先前不正确堆栈中,假如为false(默认值)则会替代先前所有不正确。

IF product_not_found THEN
      RAISE_APPLICATION_ERROR(-20123,'Invald product code'TRUE);
ENDIF;

例:阻止小于18岁的用户增加到数据库 employee 表中
CREATE OR REPALCE TRIGGER minimun_age_check BEFORE INSERT ON employee FOR EACH ROW
BEGIN
        IF ADD_MONTHS( :new.birth_date, 18*12) > SYSDATE
        THEN
               RAISE_APPLICATION_ERROR(-20001, 'Employees must at least eighteen years of age.');
        END IF;
END;

下面我们编写一个客户端程序,为了简单,同样用PL/SQL调用
DECLARE
       no_babies_allowed EXCEPTION;
       /*将名称与用于触发器中的错误号码关联起来*/
       PRAGMA EXCEPTION_INIT(no_babies_allowed, -20001);
BEGIN
         INSERT INTO employee .;
         EXCEPTION
         WHEN no_babies_allowed
         THEN
                /*
                || SQLERRM 将传递给内置过程 RAISE_APPLICATION_ERROR 的消息返回
                */
                DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
 

新折扣例子:验证两个组合是否完全相同

@SuppressWarnings("unchecked")
public String validateSetsRepeat(final String distCodes,
	final String regionCodes, final String timeCodes,
	final String businessCodes, final String cargoCodes,
	final String appendCodes, final Long setId) {
		Object object = getHibernateTemplate().execute(new HibernateCallback() {
			@SuppressWarnings("deprecation")
			public Object doInHibernate(Session session)
					throws HibernateException, SQLException {
				CallableStatement statement = session.connection().prepareCall("{call PKG_VALID_SET_INTERSECT.CDH_CHECK_REPEAT_RBTSET(?,?,?,?,?,?,?)}");
				statement.setString(1, distCodes);
				statement.setString(2, regionCodes);
				statement.setString(3, businessCodes);
				statement.setString(4, cargoCodes);
				statement.setString(5, timeCodes);
				statement.setString(6, appendCodes);
				statement.setLong(7, setId == null ? -1L : setId);
				statement.registerOutParameter(7, Types.BIGINT);
				statement.execute();
				return statement.getObject(7);
			}
		});
		return object == null ? null : object.toString();
}

PROCEDURE CDH_CHECK_REPEAT_RBTSET(DIRS     IN VARCHAR2,
                                    REGIONS  IN VARCHAR2,
                                    BUSINESS IN VARCHAR2,
                                    CARGOS   IN VARCHAR2,
                                    TIMES    IN VARCHAR2,
                                    APPENDS  IN VARCHAR2,
                                    SETID    IN OUT NUMBER) AS
    V_COUNT NUMBER;
  BEGIN
    FOR RBT_SET IN (SELECT T.SET_ID,
                           T.DIR_ID,
                           T.REGION_DMNSN_ID,
                           T.BUSINESS_DMNSN_ID,
                           T.CARGO_DMNSN_ID,
                           T.TIME_DMNSN_ID,
                           T.APPEND_DMNSN_ID
                      FROM CDH_NEWRBT_SET T
                     WHERE T.SET_ID <> DECODE(SETID, NULL, -1, SETID)) LOOP
    
      -- 判断流向是否完全相等 依据:如果A-B=B-A 表示A,B两项完全相等
      WITH DIR AS
       (SELECT D.ORG_DIST_CODE || '_' || D.DESC_DIST_CODE
          FROM CDH_NEWRBT_SET_DIR D
         WHERE D.DIR_ID = RBT_SET.DIR_ID),
      IN_DIR AS
       (SELECT * FROM TABLE(SPLIT_STR(DIRS)))
      SELECT SUM(M)
        INTO V_COUNT
        FROM (SELECT COUNT(0) AS M
                FROM (SELECT DIR.*
                        FROM DIR
                      MINUS
                      SELECT IN_DIR.* FROM IN_DIR)
              UNION
              SELECT COUNT(0)
                FROM (SELECT IN_DIR.*
                        FROM IN_DIR
                      MINUS
                      SELECT DIR.* FROM DIR));
    
      -- 判断流向是否完全相等 等于零表示A-B=B-A 全等,接下来判断其他维度是否全等
      IF V_COUNT = 0 THEN
      
        -- 判断区域类型 是否完全相同
        V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.REGION_DMNSN_ID, REGIONS);
        IF V_COUNT = 0 THEN
        
          -- 判断业务类型 是否完全相同
          V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.BUSINESS_DMNSN_ID, BUSINESS);
          IF V_COUNT = 0 THEN
          
            -- 判断快件内容 是否完全相同
            V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.CARGO_DMNSN_ID, CARGOS);
            IF V_COUNT = 0 THEN
            
              -- 判断时效 是否完全相同
              V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.TIME_DMNSN_ID, TIMES);
              IF V_COUNT = 0 THEN
              
                -- 判断附加费 是否完全相同
                V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.APPEND_DMNSN_ID,
                                             APPENDS);
                IF V_COUNT = 0 THEN
                
                  -- 运行到此处表示检测到完全相同的组合
                  SETID := RBT_SET.SET_ID;
                
                  -- 立即中断返回
                  RETURN;
                
                END IF;
              
              END IF;
            
            END IF;
          
          END IF;
        
        END IF;
      
      END IF;
    
    END LOOP;
  
    -- 如果以上循环查找未返回结果 表示没有找到完全相同的组合 程序返回NULL
    SETID := NULL;
  
  END;

开发者博客www.developsearch.com

savepoint
保存点,是事务中的一点,通过rollback可以返回到某个保存点。
一个事务中可以有多个保存点,一旦事务提交,该事务中的保存点会自动被删除,那么无论刚才做了多少个保存点,都统统没有了。
SAVEPOINT ROLLBACK_C2_POINT;
...
ROLLBACK TO ROLLBACK_C2_POINT;
 

输出异常信息

dbms_output.put_line("..................");

raise_application_error
用于在plsql使用程序中自定义不正确消息,将应用程序专有的错误从服务器端转达到客户端应用程序(其他机器上的SQLPLUS或者其他前台开发语言)。
该异常只好在数据库端的子程序(流程、函数、包、触发器)中运用,而无法在匿名块和客户端的子程序中运用。
语法为 :raise_application_error(error_number,error_msg[,[truefalse]]);
其中
error_number用于定义不正确号,该不正确号必须在-20000到-20999之间的负整数;
error_msg用于指定不正确消息,并且该消息的长度无法超过2048字节;
第三个参数假如为true,则该不正确会被放在先前不正确堆栈中,假如为false(默认值)则会替代先前所有不正确。

IF product_not_found THEN
      RAISE_APPLICATION_ERROR(-20123,'Invald product code'TRUE);
ENDIF;

例:阻止小于18岁的用户增加到数据库 employee 表中
CREATE OR REPALCE TRIGGER minimun_age_check BEFORE INSERT ON employee FOR EACH ROW
BEGIN
        IF ADD_MONTHS( :new.birth_date, 18*12) > SYSDATE
        THEN
               RAISE_APPLICATION_ERROR(-20001, 'Employees must at least eighteen years of age.');
        END IF;
END;

下面我们编写一个客户端程序,为了简单,同样用PL/SQL调用
DECLARE
       no_babies_allowed EXCEPTION;
       /*将名称与用于触发器中的错误号码关联起来*/
       PRAGMA EXCEPTION_INIT(no_babies_allowed, -20001);
BEGIN
         INSERT INTO employee .;
         EXCEPTION
         WHEN no_babies_allowed
         THEN
                /*
                || SQLERRM 将传递给内置过程 RAISE_APPLICATION_ERROR 的消息返回
                */
                DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
 

新折扣例子:验证两个组合是否完全相同

@SuppressWarnings("unchecked")
public String validateSetsRepeat(final String distCodes,
	final String regionCodes, final String timeCodes,
	final String businessCodes, final String cargoCodes,
	final String appendCodes, final Long setId) {
		Object object = getHibernateTemplate().execute(new HibernateCallback() {
			@SuppressWarnings("deprecation")
			public Object doInHibernate(Session session)
					throws HibernateException, SQLException {
				CallableStatement statement = session.connection().prepareCall("{call PKG_VALID_SET_INTERSECT.CDH_CHECK_REPEAT_RBTSET(?,?,?,?,?,?,?)}");
				statement.setString(1, distCodes);
				statement.setString(2, regionCodes);
				statement.setString(3, businessCodes);
				statement.setString(4, cargoCodes);
				statement.setString(5, timeCodes);
				statement.setString(6, appendCodes);
				statement.setLong(7, setId == null ? -1L : setId);
				statement.registerOutParameter(7, Types.BIGINT);
				statement.execute();
				return statement.getObject(7);
			}
		});
		return object == null ? null : object.toString();
}

PROCEDURE CDH_CHECK_REPEAT_RBTSET(DIRS     IN VARCHAR2,
                                    REGIONS  IN VARCHAR2,
                                    BUSINESS IN VARCHAR2,
                                    CARGOS   IN VARCHAR2,
                                    TIMES    IN VARCHAR2,
                                    APPENDS  IN VARCHAR2,
                                    SETID    IN OUT NUMBER) AS
    V_COUNT NUMBER;
  BEGIN
    FOR RBT_SET IN (SELECT T.SET_ID,
                           T.DIR_ID,
                           T.REGION_DMNSN_ID,
                           T.BUSINESS_DMNSN_ID,
                           T.CARGO_DMNSN_ID,
                           T.TIME_DMNSN_ID,
                           T.APPEND_DMNSN_ID
                      FROM CDH_NEWRBT_SET T
                     WHERE T.SET_ID <> DECODE(SETID, NULL, -1, SETID)) LOOP
    
      -- 判断流向是否完全相等 依据:如果A-B=B-A 表示A,B两项完全相等
      WITH DIR AS
       (SELECT D.ORG_DIST_CODE || '_' || D.DESC_DIST_CODE
          FROM CDH_NEWRBT_SET_DIR D
         WHERE D.DIR_ID = RBT_SET.DIR_ID),
      IN_DIR AS
       (SELECT * FROM TABLE(SPLIT_STR(DIRS)))
      SELECT SUM(M)
        INTO V_COUNT
        FROM (SELECT COUNT(0) AS M
                FROM (SELECT DIR.*
                        FROM DIR
                      MINUS
                      SELECT IN_DIR.* FROM IN_DIR)
              UNION
              SELECT COUNT(0)
                FROM (SELECT IN_DIR.*
                        FROM IN_DIR
                      MINUS
                      SELECT DIR.* FROM DIR));
    
      -- 判断流向是否完全相等 等于零表示A-B=B-A 全等,接下来判断其他维度是否全等
      IF V_COUNT = 0 THEN
      
        -- 判断区域类型 是否完全相同
        V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.REGION_DMNSN_ID, REGIONS);
        IF V_COUNT = 0 THEN
        
          -- 判断业务类型 是否完全相同
          V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.BUSINESS_DMNSN_ID, BUSINESS);
          IF V_COUNT = 0 THEN
          
            -- 判断快件内容 是否完全相同
            V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.CARGO_DMNSN_ID, CARGOS);
            IF V_COUNT = 0 THEN
            
              -- 判断时效 是否完全相同
              V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.TIME_DMNSN_ID, TIMES);
              IF V_COUNT = 0 THEN
              
                -- 判断附加费 是否完全相同
                V_COUNT := COUNT_DMNSN_MINUS(RBT_SET.APPEND_DMNSN_ID,
                                             APPENDS);
                IF V_COUNT = 0 THEN
                
                  -- 运行到此处表示检测到完全相同的组合
                  SETID := RBT_SET.SET_ID;
                
                  -- 立即中断返回
                  RETURN;
                
                END IF;
              
              END IF;
            
            END IF;
          
          END IF;
        
        END IF;
      
      END IF;
    
    END LOOP;
  
    -- 如果以上循环查找未返回结果 表示没有找到完全相同的组合 程序返回NULL
    SETID := NULL;
  
  END;

开发者博客www.developsearch.com

猜你喜欢

转载自keepwork.iteye.com/blog/1994092