以文本方式查看主题

-  课外天地 李树青  (http://www.njcie.com/bbs/index.asp)
--  数据库系统原理课件  (http://www.njcie.com/bbs/list.asp?boardid=19)
----  [推荐]Oracle数据库PL-SQL学习资料之七——子程序、包和触发器  (http://www.njcie.com/bbs/dispbbs.asp?boardid=19&id=608)

--  作者:admin
--  发布时间:2008/11/21 19:14:51
--  [推荐]Oracle数据库PL-SQL学习资料之七——子程序、包和触发器

9 子程序
PL/SQL语句块分为匿名和命名两种,匿名语句块每次编译运行且不能存储于数据库中,而且由于没有名称,所以也没法调用。
命名语句块显然具有相反的特点,具体包含过程、函数、包和触发器,其中过程和函数统称为子程序。

9、1 子程序
包含过程和函数

9、1、1 简单使用方法
1)过程的使用
创建为:
set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE HelloWorldProc
AS
BEGIN
  dbms_output.put_line(\'Hello World!\');
END HelloWorldProc;
/

调用为:
BEGIN
  HelloWorldProc;
END;
/

2)函数的使用
和过程很相似,但是具有返回值,可以放入表达式进行调用,创建为:
CREATE OR REPLACE FUNCTION HelloWorldFunc
RETURN varchar IS
BEGIN
  return \'Hello World!\';
END HelloWorldFunc;
/

使用为:
set serveroutput on size 1000000;
begin
  dbms_output.put_line(helloworldfunc());
end;
/

说明:replace表示如果存在对象则覆盖。如果再次编译出错,则对象被删除而没被创建。

9、1、2 子程序参数

9、1、2、1 简单使用
1)具有参数的过程
set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE MyProc
(
  p_str varchar
)
AS
BEGIN
dbms_output.put_line(p_str);
END MyProc;
/

BEGIN
  MyProc(\'Hello World!\');
END;
/

复杂的例子:
set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE SelectStudent
(
  p_sid  in stu.sid%TYPE
)
AS
v_sname varchar(8);
BEGIN
  select name into v_sname from stu where sid=p_sid;
  DBMS_OUTPUT.PUT_LINE(v_sname);
END SelectStudent;
/

BEGIN
  SelectStudent(1);
END;
/

2)具有参数的函数
CREATE OR REPLACE FUNCTION MyFunc
(
  p_str varchar
)
RETURN varchar IS
BEGIN
  return upper(p_str);
END MyFunc;
/

set serveroutput on size 1000000;
begin
  dbms_output.put_line(MyFunc(\'hello world!\'));
end;
/

注意:不能在子程序中约束参数,如长度和精度等,这些约束信息是从实际参数动态获取的。如下面的定义有误:
CREATE OR REPLACE FUNCTION MyFunc
(
  p_str varchar(6)
)
RETURN varchar IS
BEGIN
  return upper(p_str);
END MyFunc;
/

但是,由于没有形式参数约束,所以在实际参数赋值时可能会产生运行时错误,如:
CREATE OR REPLACE FUNCTION MyFunc
(
  p_str out varchar
)
RETURN varchar IS
BEGIN
  p_str:=\'1234567890\';
  return upper(p_str);
END MyFunc;
/

set serveroutput on size 1000000;

declare
  str varchar(6);
begin
  dbms_output.put_line(MyFunc(str));
end;
/

错误为:
declare
*
ERROR 位于第 1 行:
ORA-06502: PL/SQL: 数字或值错误 :  字符串缓冲区太小
ORA-06512: 在"SYS.MYFUNC", line 7
ORA-06512: 在line 4

也可以使用%type属性来获取其他变量的类型,虽然在形式参数中可以以此来增加参数约束,但是系统仍然会可能产生运行时错误,如:
CREATE OR REPLACE PACKAGE Globals AS
  v_str varchar(6);
END Globals;
/

CREATE OR REPLACE FUNCTION MyFunc
(
  p_str out Globals.v_str%type
)
RETURN varchar IS
BEGIN
  p_str:=\'1234567890\';
  return upper(p_str);
END MyFunc;
/


set serveroutput on size 1000000;

declare
  str Globals.v_str%type;
begin
  dbms_output.put_line(MyFunc(str));
end;
/


9、1、2、2 参数模式
形式参数具有三种模式,即in,out和in out。默认为in。
一般而言,函数不应该使用out模式,子程序可以使用out模式返回多个形式参数值

1)in模式
在in模式下,形式参数象常量,只读而不能被改变。所以当调用结束,实际参数不变。

set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE ModeTest
(
  p_Parameter    IN NUMBER) IS
  v_LocalVariable  NUMBER := 0;
BEGIN
  IF (p_Parameter IS NULL) THEN
    dbms_output.put_line(\'p_Parameter is NULL\');
  ELSE
    dbms_output.put_line(\'p_Parameter = \' || p_Parameter);
  END IF;

  v_LocalVariable := p_Parameter;  -- Legal
  --p_InParameter := 7;  -- Illegal

END ModeTest;
/

set serveroutput on size 1000000;
declare
  num number;
BEGIN
  num:=1;
  ModeTest(num);
  dbms_output.put_line(num);
END;
/

2)out模式
在out模式下,实际参数传来的值被忽略,形式参数是个永远未被初始化的变量,具有空值,可读可写,调用结束后,形式参数的值被赋给实际参数。

set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE ModeTest
(
  p_Parameter    OUT NUMBER) IS
  v_LocalVariable  NUMBER := 0;
BEGIN
  IF (p_Parameter IS NULL) THEN
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter is NULL\');
  ELSE
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter = \' || p_Parameter);
  END IF;

  /* Assign 7 to p_Parameter. This is legal, since we are writing to an OUT parameter. */
  p_Parameter := 7;  -- Legal

  /* Assign p_Parameter to v_LocalVariable. In Oracle7 version
     7.3.4, and Oracle8 version 8.0.4 or higher (including 8i),
     this is legal.  Prior to 7.3.4, it is illegal to read from an
     OUT parameter. */
  v_LocalVariable := p_Parameter;  -- Possibly illegal
END ModeTest;
/

set serveroutput on size 1000000;
declare
  num number;
BEGIN
  num:=1;
  ModeTest(num);
  dbms_output.put_line(num);
END;
/

注意:如果子程序内部发生异常,即便是已经改变形式参数,形式参数的值也不会被赋给实际参数
set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE ModeTest
(
  p_Parameter    OUT NUMBER) IS
  v_LocalVariable  NUMBER := 0;
BEGIN
  IF (p_Parameter IS NULL) THEN
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter is NULL\');
  ELSE
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter = \' || p_Parameter);
  END IF;

  /* Assign 7 to p_Parameter. This is legal, since we are writing to an OUT parameter. */
  p_Parameter := 7;  -- Legal

  /* Assign p_Parameter to v_LocalVariable. In Oracle7 version
     7.3.4, and Oracle8 version 8.0.4 or higher (including 8i),
     this is legal.  Prior to 7.3.4, it is illegal to read from an
     OUT parameter. */
  v_LocalVariable := p_Parameter;  -- Possibly illegal
  v_LocalVariable:=1/0;

END ModeTest;
/

set serveroutput on size 1000000;
declare
  num number;
BEGIN
  num:=1;
  ModeTest(num);  
EXCEPTION
  WHEN others THEN
    --DBMS_OUTPUT.PUT_LINE(SQLCODE());
    --DBMS_OUTPUT.PUT_LINE(substr(SQLERRM(),1,200));
    dbms_output.put_line(num);
END;
/

另外,out模式下的实际参数不能是常量或者表达式,因为没法赋值,会导致错误

3)in out模式
是in模式和out模式的组合,实际参数的值可以传给形式参数,同时在调用结束后,形式参数的值也被赋给实际参数。

set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE ModeTest
(
  p_Parameter    IN OUT NUMBER) IS
  v_LocalVariable  NUMBER := 0;
BEGIN
  IF (p_Parameter IS NULL) THEN
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter is NULL\');
  ELSE
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter = \' || p_Parameter);
  END IF;

  /* Assign p_Parameter to v_LocalVariable. This is legal, since we are reading from an IN OUT parameter. */
  v_LocalVariable := p_Parameter;

  /* Assign 7 to p_Parameter. This is legal, since we are writing to an IN OUT parameter. */
  p_Parameter := 7;

END ModeTest;
/

set serveroutput on size 1000000;
declare
  num number;
BEGIN
  num:=1;
  ModeTest(num);
  dbms_output.put_line(num);
END;
/

9、1、2、3 引用传递和数值传递
缺省情况下,in参数使用引用传递,而in out和out参数使用数值传递
如果显式使用引用传递方式来访问in out和out参数,可以使用NOCOPY编译器提示,此时将会导致形式参数的改变会影响实际参数,甚至在异常发生后也会产生形式参数的改变影响实际参数。
set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE ModeTest
(
  p_Parameter    OUT NOCOPY NUMBER) IS
  v_LocalVariable  NUMBER := 0;
BEGIN
  IF (p_Parameter IS NULL) THEN
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter is NULL\');
  ELSE
    DBMS_OUTPUT.PUT_LINE(\'p_Parameter = \' || p_Parameter);
  END IF;

  p_Parameter := 7;  -- Legal

  v_LocalVariable := p_Parameter;
  v_LocalVariable:=1/0;

END ModeTest;
/

set serveroutput on size 1000000;
declare
  num number;
BEGIN
  num:=1;
  ModeTest(num);  
EXCEPTION
  WHEN others THEN
    --DBMS_OUTPUT.PUT_LINE(SQLCODE());
    --DBMS_OUTPUT.PUT_LINE(substr(SQLERRM(),1,200));
    dbms_output.put_line(num);
END;
/

输出为:
p_Parameter is NULL
7

注意:即便是使用了NOCOPY,并不代表原有的实际参数值会传递到形式参数中,而只能说明形式参数的改变会导致实际参数的改变。

使用NOCOPY方式参数传递速度更快,如:
set serveroutput on size 1000000;

CREATE OR REPLACE PACKAGE CopyFast AS
  TYPE StudentArray IS TABLE OF stu%ROWTYPE;

  PROCEDURE PassStudents1(p_Parameter IN StudentArray);
  PROCEDURE PassStudents2(p_Parameter IN OUT StudentArray);
  PROCEDURE PassStudents3(p_Parameter IN OUT NOCOPY StudentArray);

  PROCEDURE Go;
END CopyFast;
/

show errors

CREATE OR REPLACE PACKAGE BODY CopyFast AS
  PROCEDURE PassStudents1(p_Parameter IN StudentArray) IS
  BEGIN
    NULL;
  END PassStudents1;

  PROCEDURE PassStudents2(p_Parameter IN OUT StudentArray) IS
  BEGIN
    NULL;
  END PassStudents2;

  PROCEDURE PassStudents3(p_Parameter IN OUT NOCOPY StudentArray) IS
  BEGIN
    NULL;
  END PassStudents3;

  PROCEDURE Go IS
    v_StudentArray StudentArray := StudentArray(NULL);
    v_StudentRec stu%ROWTYPE;
    v_Time1 NUMBER;
    v_Time2 NUMBER;
    v_Time3 NUMBER;
    v_Time4 NUMBER;
  BEGIN
    -- Fill up the array with 50,001 copies of David Dinsmore\'s
    -- record.
    SELECT *
      INTO v_StudentArray(1)
      FROM stu
      WHERE sid = 1;
    v_StudentArray.EXTEND(1, 1);

    -- Call each version of PassStudents, and time them.
    -- DBMS_UTILITY.GET_TIME will return the current time, in
    -- hundredths of a second.
    v_Time1 := DBMS_UTILITY.GET_TIME;
    PassStudents1(v_StudentArray);
    v_Time2 := DBMS_UTILITY.GET_TIME;
    PassStudents2(v_StudentArray);
    v_Time3 := DBMS_UTILITY.GET_TIME;
    PassStudents3(v_StudentArray);
    v_Time4 := DBMS_UTILITY.GET_TIME;

    -- Output the results.
    DBMS_OUTPUT.PUT_LINE(\'Time to pass IN: \' || TO_CHAR((v_Time2 - v_Time1) / 100));
    DBMS_OUTPUT.PUT_LINE(\'Time to pass IN OUT: \' || TO_CHAR((v_Time3 -   v_Time2) / 100));
    DBMS_OUTPUT.PUT_LINE(\'Time to pass IN OUT NOCOPY: \' || TO_CHAR((v_Time4 - v_Time3) / 100));
  END Go;
END CopyFast;
/
show errors

BEGIN
  CopyFast.Go;
END;
/

9、1、2、4 没有参数的子程序
如果子程序或者函数没有参数,则在声明和调用时不使用括号,如:
set serveroutput on size 1000000;

CREATE OR REPLACE PROCEDURE NoParamsP AS
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'No Paramaters!\');
END NoParamsP;
/

CREATE OR REPLACE FUNCTION NoParamsF
  RETURN DATE AS
BEGIN
  RETURN SYSDATE;
END NoParamsF;
/

BEGIN
  NoParamsP;
  DBMS_OUTPUT.PUT_LINE(\'Calling NoParamsF on \' || TO_CHAR(NoParamsF, \'DD-MON-YYYY\'));
END;
/

9、1、2、5 使用命名符
一般的子程序都使用定位符来传递实际参数,为了方便也可以使用命名符来赋值,如:
CREATE OR REPLACE PROCEDURE CallMe
(
  p_ParameterA VARCHAR2,
  p_ParameterB NUMBER,
  p_ParameterC BOOLEAN,
  p_ParameterD DATE
)
AS
BEGIN
  NULL;
END CallMe;
/


DECLARE
  v_Variable1 VARCHAR2(10);
  v_Variable2 NUMBER(7,6);
  v_Variable3 BOOLEAN;
  v_Variable4 DATE;
BEGIN
  CallMe( p_ParameterC => v_Variable3, p_ParameterD => v_Variable4,p_ParameterA => v_Variable1, p_ParameterB => v_Variable2);
END;
/

注意
1)可以重新调整参数位置
2)可以混合使用定位符和命名符,但是命名符只能在定位符后使用,如:
DECLARE
  v_Variable1 VARCHAR2(10);
  v_Variable2 NUMBER(7,6);
  v_Variable3 BOOLEAN;
  v_Variable4 DATE;
BEGIN
   CallMe(v_Variable1, v_Variable2,p_ParameterC => v_Variable3,p_ParameterD => v_Variable4);
END;
/

9、1、2、6 使用参数缺省值
CREATE OR REPLACE PROCEDURE AddNewStudent
(
  p_Sid  stu.sid%TYPE,
  p_Name stu.name%TYPE DEFAULT \'NONE\',
  p_Sex  stu.sex%TYPE DEFAULT \'M\'
)
AS
BEGIN
  INSERT INTO stu VALUES (p_Sid, p_Name, p_Sex);
END AddNewStudent;
/

BEGIN
  AddNewStudent(200,\'Simon\',\'M\');
  AddNewStudent(300);
END;
/

select * from stu;
delete from stu where sid>100

注意:
1)缺省值一般只能从尾到头来定义,调用时的缺省值也只能是最后几个参数,如果跳跃定义,必须通过命名符来访问参数,如:
CREATE OR REPLACE PROCEDURE AddNewStudent
(
  p_Sid  stu.sid%TYPE,
  p_Name stu.name%TYPE DEFAULT \'NONE\',
  p_Sex  stu.sex%TYPE
)
AS
BEGIN
  INSERT INTO stu VALUES (p_Sid, p_Name, p_Sex);
END AddNewStudent;
/

BEGIN
  AddNewStudent(p_Sid=>400, p_Sex=>\'F\');
END;
/

2)default关键词可以使用“:=”代替

9、1、3 CALL语句
可以直接在SQL语句块中调用子程序,但是不能在PL/SQL语句块中调用子程序,如果需要在PL/SQL语句块中使用CALL,需要使用动态SQL语句,如:
set serveroutput on size 1000000;
CREATE OR REPLACE PROCEDURE CallProc1(p1 IN VARCHAR2 := NULL) AS
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'CallProc1 called with \' || p1);
END CallProc1;
/

CREATE OR REPLACE PROCEDURE CallProc2(p1 IN OUT VARCHAR2) AS
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'CallProc2 called with \' || p1);
  p1 := p1 || \' returned!\';
END CallProc2;
/

CREATE OR REPLACE FUNCTION CallFunc(p1 IN VARCHAR2)
  RETURN VARCHAR2 AS
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'CallFunc called with \' || p1);
  RETURN p1;
END CallFunc;
/

-- Some valid calls direct from SQL.
CALL CallProc1(\'Hello!\');
CALL CallProc1();
VARIABLE v_Output VARCHAR2(50);
CALL CallFunc(\'Hello!\') INTO :v_Output;
PRINT v_Output
CALL CallProc2(:v_Output);
PRINT v_Output

-- This is illegal
--BEGIN
-- CALL CallProc1();
--END;
--/

-- But these are legal
DECLARE
  v_Result VARCHAR2(50);
BEGIN
  EXECUTE IMMEDIATE \'CALL CallProc1(\'\'Hello from PL/SQL\'\')\';
  EXECUTE IMMEDIATE
    \'CALL CallFunc(\'\'Hello from PL/SQL\'\') INTO :v_Result\' USING OUT v_Result;
END;
/

9、2 包
包是相关对象的共同存储体,包含两个部分,一是包头,即包的说明,另一个是包体,用于定义包中成员。这两个部分是分开定义和存储的。

相对于子程序而言,特点为:
1)封装相关对象
2)包只能存储于全局,不可能为局部
3)依赖性要小于子程序
4)性能较好

9、2、1 定义
9、2、1、1 包头
如:
CREATE OR REPLACE PACKAGE StuPackage AS
  PROCEDURE AddStudent(p_SID IN stu.sid%TYPE,p_Name IN stu.name%TYPE,p_sex IN stu.sex%TYPE);  
  PROCEDURE RemoveStudent(p_SID IN stu.sid%TYPE);
  e_StudentNotRegistered EXCEPTION;
  TYPE t_StudentIDTable IS TABLE OF stu.sid%TYPE INDEX BY BINARY_INTEGER;
END StuPackage;
/

注意:代码显示红色是因为把RemoveStudent中的Rem当成注释

9、2、1、2 包体
如:
CREATE OR REPLACE PACKAGE BODY StuPackage AS
  PROCEDURE AddStudent(p_SID IN stu.sid%TYPE,p_Name IN stu.name%TYPE,p_Sex IN stu.sex%TYPE)
  is
  begin
      insert into stu values(p_SID,p_Name,p_Sex);
  end AddStudent;
  
  PROCEDURE RemoveStudent(p_SID IN stu.sid%TYPE)
  is
  begin
      delete from stu where sid=p_SID;
      IF SQL%NOTFOUND THEN
      RAISE e_StudentNotRegistered;
    END IF;
  end RemoveStudent;
END StuPackage;
/

说明:
1)包体独立定义和编译,当然要保证包头先行定义和编译
2)包体不是必须的,此时的包充当封装全局变量的作用
3)包体的定义要和包头的定义原型一致

9、2、2 使用包
9、2、2、1 一般使用
begin
  StuPackage.AddStudent(900,\'胡歌\',\'M\');
end;
/

select * from stu;

begin
  StuPackage.RemoveStudent(900);
end;
/

select * from stu;

9、2、2、2 包内部成员的使用
上述包中的异常变量可以在包外部使用,如:
set serveroutput on size 1000000;

begin
  RAISE StuPackage.e_StudentNotRegistered;
EXCEPTION
  WHEN others THEN
  DBMS_OUTPUT.PUT_LINE(\'Error\');
end;
/

但是下面包中定义的成员不能在包外部使用,如:
CREATE OR REPLACE PACKAGE StuPackage AS
  PROCEDURE AddStudent(p_SID IN stu.sid%TYPE,p_Name IN stu.name%TYPE,p_sex IN stu.sex%TYPE);  
  PROCEDURE RemoveStudent(p_SID IN stu.sid%TYPE);
  TYPE t_StudentIDTable IS TABLE OF stu.sid%TYPE INDEX BY BINARY_INTEGER;
END StuPackage;
/

CREATE OR REPLACE PACKAGE BODY StuPackage
as
  e_StudentNotRegistered EXCEPTION;

  PROCEDURE AddStudent(p_SID IN stu.sid%TYPE,p_Name IN stu.name%TYPE,p_Sex IN stu.sex%TYPE)
  is
  begin
      insert into stu values(p_SID,p_Name,p_Sex);
  end AddStudent;
  
  PROCEDURE RemoveStudent(p_SID IN stu.sid%TYPE)
  is
  begin
      delete from stu where sid=p_SID;
      IF SQL%NOTFOUND THEN
      RAISE e_StudentNotRegistered;
    END IF;
  end RemoveStudent;
END StuPackage;
/

此时会出错,如:
set serveroutput on size 1000000;

begin
  RAISE StuPackage.e_StudentNotRegistered;
EXCEPTION
  WHEN others THEN
  DBMS_OUTPUT.PUT_LINE(\'Error\');
end;
/

9、2、2、3 包成员的重载
set serveroutput on size 1000000;

CREATE OR REPLACE TYPE t1 AS OBJECT
(
  f NUMBER
);
/
show errors

CREATE OR REPLACE TYPE t2 AS OBJECT
(
  f NUMBER
);
/
show errors

CREATE OR REPLACE PACKAGE Overload AS
  PROCEDURE Proc(p_Parameter1 IN t1);
  PROCEDURE Proc(p_Parameter1 IN t2);
END Overload;
/
show errors

CREATE OR REPLACE PACKAGE BODY Overload AS
  PROCEDURE Proc(p_Parameter1 IN t1) IS
  BEGIN
    DBMS_OUTPUT.PUT_LINE(\'Proc(t1): \' || p_Parameter1.f);
  END Proc;

  PROCEDURE Proc(p_Parameter1 IN t2) IS
  BEGIN
    DBMS_OUTPUT.PUT_LINE(\'Proc(t2): \' || p_Parameter1.f);
  END Proc;
END Overload;
/
show errors

set serveroutput on

DECLARE
  v_Obj1 t1 := t1(1);
  v_Obj2 t2 := t2(2);
BEGIN
  Overload.Proc(v_Obj1);
  Overload.Proc(v_Obj2);
END;
/

9、2、2、4 包的初始化
直接在包体中使用begin和end来封装初始化代码,如:
CREATE OR REPLACE PACKAGE Random AS
  -- Random number generator.  Uses the same algorithm as the
  -- rand() function in C.

  -- Used to change the seed.  From a given seed, the same
  -- sequence of random numbers will be generated.
  PROCEDURE ChangeSeed(p_NewSeed IN NUMBER);

  -- Returns a random integer between 1 and 32767.
  FUNCTION Rand RETURN NUMBER;

  -- Same as Rand, but with a procedural interface.
  PROCEDURE GetRand(p_RandomNumber OUT NUMBER);

  -- Returns a random integer between 1 and p_MaxVal.
  FUNCTION RandMax(p_MaxVal IN NUMBER) RETURN NUMBER;

  -- Same as RandMax, but with a procedural interface.
  PROCEDURE GetRandMax(p_RandomNumber OUT NUMBER,
                       p_MaxVal IN NUMBER);
END Random;
/

CREATE OR REPLACE PACKAGE BODY Random AS

  /* Used for calculating the next number. */
  v_Multiplier  CONSTANT NUMBER := 22695477;
  v_Increment   CONSTANT NUMBER := 1;

  /* Seed used to generate random sequence. */
  v_Seed        number := 1;

  PROCEDURE ChangeSeed(p_NewSeed IN NUMBER) IS
  BEGIN
    v_Seed := p_NewSeed;
  END ChangeSeed;

  FUNCTION Rand RETURN NUMBER IS
  BEGIN
    v_Seed := MOD(v_Multiplier * v_Seed + v_Increment,
                  (2 ** 32));
    RETURN BITAND(v_Seed/(2 ** 16), 32767);
  END Rand;

  PROCEDURE GetRand(p_RandomNumber OUT NUMBER) IS
  BEGIN
    -- Simply call Rand and return the value.
    p_RandomNumber := Rand;
  END GetRand;

  FUNCTION RandMax(p_MaxVal IN NUMBER) RETURN NUMBER IS
  BEGIN
    RETURN MOD(Rand, p_MaxVal) + 1;
  END RandMax;

  PROCEDURE GetRandMax(p_RandomNumber OUT NUMBER,
                       p_MaxVal IN NUMBER) IS
  BEGIN
    -- Simply call RandMax and return the value.
    p_RandomNumber := RandMax(p_MaxVal);
  END GetRandMax;

BEGIN
  /* Package initialization.  Initialize the seed to the current
     time in seconds. */
  ChangeSeed(TO_NUMBER(TO_CHAR(SYSDATE, \'SSSSS\')));
END Random;
/

说明:SSSSS意思为“Seconds past midnight”

使用为:
set serveroutput on size 1000000;
declare
  p_rand number;
begin
  Random.GetRand(p_rand);
  DBMS_OUTPUT.PUT_LINE(p_rand);
  Random.GetRandMax(p_rand,6);
  DBMS_OUTPUT.PUT_LINE(p_rand);
  
  DBMS_OUTPUT.PUT_LINE(Random.rand);
  DBMS_OUTPUT.PUT_LINE(Random.randmax(100));
  
end;
/

9、3 触发器
触发器和子程序相似,区别在于
1)不能定义为局部,子程序可以,方法为:
set serveroutput on size 1000000;
declare
  p_rand number;
  
  PROCEDURE NoParamsP as
  BEGIN
    DBMS_OUTPUT.PUT_LINE(\'No Paramaters!\');
  END NoParamsP;

begin
  NoParamsP;
end;
/

2)主要通过事件来隐式触发,无需显示调用
3)不接受参数
4)不能对SYS对象做触发器,所以新建新的其他用户表,如:
create table Scott.stu
(
    sid integer primary key,
    name varchar(8) not null,
    sex char(1) check(sex in(\'M\',\'F\'))
);

insert into scott.stu values(1,\'Tom\',\'M\');
insert into scott.stu values(2,\'Alice\',\'F\');
insert into scott.stu values(3,\'eric\',\'M\');

5)触发器不能超过32K,如果更大,需要将代码封装进包或者存储过程,在触发器中调用它们

9、3、1 触发器类型
Oracle的触发器按照触发方式可以分为三种,分别为DML触发器、instead-of触发器和系统触发器。

9、3、1、1 DML触发器
9、3、1、1、1 简单使用
CREATE OR REPLACE TRIGGER scott.OperatingStu
  AFTER INSERT OR DELETE OR UPDATE ON scott.stu
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'OK!\');
END OperatingStu;
/

观察为:
insert into scott.stu values(100,\'Jim\',\'M\');

9、3、1、1、2 详细定义
DML触发器的定义有三个要点
1)可以对三种DML操作进行定义,如insert、update和delete
2)可以指定两个时机,如after和before
3)可以指定行级或者语句级,默认为语句级,如为行级,则每条操作记录都会触发一次触发器,如:
CREATE OR REPLACE TRIGGER scott.OperatingStu
  AFTER INSERT OR DELETE OR UPDATE ON scott.stu
  for each row
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'OK!\');
END OperatingStu;
/

此时更新多条记录会导致多次触发,如:
update scott.stu set name=upper(name);

9、3、1、1、3 触发顺序
1)如果同一个表有多个相同触发器,触发的顺序应该是先触发新的触发器,后激活旧的触发器,如:
set serveroutput on size 1000000;

CREATE OR REPLACE TRIGGER scott.OperatingStuAlso
  AFTER INSERT OR DELETE OR UPDATE ON scott.stu
  for each row
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'OK!!\');
END OperatingStuAlso;
/

可以观察为:
update scott.stu set name=upper(name);

2)如果同一个表存在行级和语句级的触发器,触发顺序为(不考虑不存在的情况):
语句级别之前的触发器
每一行之前的触发器
执行语句操作行
每一行之后的触发器
语句级别之后的触发器

9、3、1、1、4 相关标识符
触发器中可以使用:old和:new两个相关标识符,它们是系统提供的PL/SQL绑定变量,分别保存更新前的表记录信息和更新后的表记录信息

注意:
1)可以更新:new,将会改写更新信息,如:
CREATE OR REPLACE TRIGGER GenerateStudentID
  BEFORE INSERT OR UPDATE ON scott.stu
  FOR EACH ROW
BEGIN
  SELECT max(sid)+1 INTO :new.sid FROM scott.stu;
END GenerateStudentID;
/

insert into scott.stu(name,sex) values(\'Lee\',\'M\');
select * from scott.stu;

此时即便是指定sid也会被改写,如:
insert into scott.stu values(1000,\'Lee\',\'M\');
select * from scott.stu;

2)只能在行级别触发器才能使用:old和:new两个相关标识符,因为语句触发器会影响多行
3):old不能修改,只能够读取,:new只能在行级别之前触发器中被修改,不能在行级别之后触发器中被修改
4):old和:new两个相关标识符都是记录类型,可以理解为triggering_table%rowtype类型,但是实际上不是记录,所以不能作为一般记录来处理,如不能整个记录来进行赋值,不能当作参数赋予子程序
5)对于insert操作的:old和delete操作的:new,相关信息都为null
6)可以使用REFERENCING替换默认的相关标识符名称(注意位置),如:
CREATE OR REPLACE TRIGGER GenerateStudentID
  BEFORE INSERT OR UPDATE ON scott.stu
  REFERENCING new as new_table
  FOR EACH ROW
BEGIN
  SELECT max(sid)+1 INTO :new_table.sid FROM scott.stu;
END GenerateStudentID;
/
7)还有:parent相关标识符标识嵌套表所对应的父表当前行

9、3、1、1、5 指定触发的条件(WHEN)
set serveroutput on size 1000000;
CREATE OR REPLACE TRIGGER scott.OperatingStu
  AFTER INSERT OR DELETE OR UPDATE ON scott.stu
  for each row
  when (new.sex=\'M\')
BEGIN
  DBMS_OUTPUT.PUT_LINE(\'OK!\');
END OperatingStu;
/

注意:在when语句中的相关标识符不用加冒号

上述触发器只会在操作男生记录时触发,如:
update scott.stu set name=upper(name);

当然,也可以写为:
set serveroutput on size 1000000;
CREATE OR REPLACE TRIGGER scott.OperatingStu
  AFTER INSERT OR DELETE OR UPDATE ON scott.stu
  for each row
BEGIN
  if :new.sex=\'M\' then
    DBMS_OUTPUT.PUT_LINE(\'OK!\');
  end if;
END OperatingStu;
/

9、3、1、1、6 触发器谓词
通过INSERTING、UPDATING和DELETING函数来判断触发类型,如:
set serveroutput on size 1000000;

CREATE OR REPLACE TRIGGER scott.OperatingStu
  AFTER INSERT OR DELETE OR UPDATE ON scott.stu
  for each row
BEGIN
  if INSERTING then
    DBMS_OUTPUT.PUT_LINE(\'INSERTING\');
  elsif UPDATING then
    DBMS_OUTPUT.PUT_LINE(\'UPDATING\');
  elsif DELETING then
    DBMS_OUTPUT.PUT_LINE(\'DELETING\');
  end if;
END OperatingStu;
/

update scott.stu set name=upper(name);

9、3、1、2 instead-of触发器
用于给视图创建触发器,对于不能直接更新的试图通过此触发器来使之能够更新,如创建新的视图:
create or replace view scott.new_stu
as select upper(name) upper from scott.stu;

select * from scott.new_stu;

此时不能插入更新,如:
insert into scott.new_stu values(\'Jim\');

建立instead-of触发器,如:
create or replace trigger scott.insertView
instead of insert on scott.new_stu
declare
  p_num integer;
begin
  select max(sid) into p_num from scott.stu;
  insert into scott.stu values(p_num+1,:new.upper,\'M\');
end;
/

注意:所有的instead-of触发器都是行级的,for each row可写可不写

此时可以更新,如:
insert into scott.new_stu values(\'Jim\');

select * from scott.new_stu;
select * from scott.stu;

说明:
视图在没有下面几项内容的时候,通常可以更新:
1)集合操作符(union等)
2)聚合运算函数(sum等)
3)group by,connect by和start with
4)distinct操作符
5)连接

9、3、1、3 系统触发器
9、3、1、3、1 一般使用
当发生诸如数据库关闭或者启动之类的系统事件和执行DDL语句时,激发系统触发器

下面的系统触发器可以记录任何创建的数据字典对象信息,如:
建立信息储存表,如:
CREATE TABLE scott.ddl_creations
(
  user_id       VARCHAR2(30),
  object_type   VARCHAR2(20),
  object_name   VARCHAR2(30),
  object_owner  VARCHAR2(30),
  creation_date DATE
);

创建系统触发器,如:
CREATE OR REPLACE TRIGGER scott.LogCreations
AFTER CREATE ON DATABASE
BEGIN
  INSERT INTO ddl_creations (user_id, object_type, object_name,object_owner, creation_date)VALUES (USER, SYS.DICTIONARY_OBJ_TYPE, SYS.DICTIONARY_OBJ_NAME,SYS.DICTIONARY_OBJ_OWNER, SYSDATE);
END LogCreations;
/

创建新表,如:
create table stubak
(
    sid integer primary key,
    name varchar(8) not null,
    sex char(1) check(sex in(\'M\',\'F\'))
);

查询:
select * from scott.ddl_creations;
drop table stubak;

9、3、1、3、2 数据库级别触发器和模式级别触发器
系统触发器可以在数据库级别和模式级别进行定义,其中只要有事件发生就会触发数据库级别触发器,而只有以指定模式发生事件时才会触发模式级别触发器。默认为该用户模式对应的模式触发器。
注意:DML和instead_of触发器没有模式之分

set serveroutput on size 1000000;

建立UserA和UserB帐户
Connect system/system@school AS SYSDBA

CREATE USER UserA IDENTIFIED BY UserA;
GRANT connect, resource, ADMINISTER DATABASE TRIGGER TO UserA;

CREATE USER UserB IDENTIFIED BY UserB;
GRANT connect, resource, ADMINISTER DATABASE TRIGGER TO UserB;

create table temp_table
(
  info varchar2(100)
);

grant all on temp_table to UserA;
grant all on temp_table to UserB;

以UserA帐户登录创建模式触发器
connect UserA/UserA
CREATE OR REPLACE TRIGGER LogUserAConnects AFTER LOGON ON SCHEMA
BEGIN
  insert into sys.temp_table values(\'LogUserAConnects fired!\');
END LogUserAConnects;
/

以UserB帐户登录创建模式触发器
connect UserB/UserB
CREATE OR REPLACE TRIGGER LogUserBConnects AFTER LOGON ON SCHEMA
BEGIN
  insert into sys.temp_table values(\'LogUserBConnects fired!\');
END LogUserBConnects;
/

以system帐户登录创建数据库触发器
Connect system/system@school AS SYSDBA

CREATE OR REPLACE TRIGGER LogAllConnects AFTER LOGON ON DATABASE
BEGIN
  insert into sys.temp_table values(\'LogAllConnects fired!\');
END LogAllConnects;
/

以三个帐户分别登录三次
connect UserA/UserA;
connect UserB/UserB;
Connect system/system@school AS SYSDBA;

查看temp_table表信息
select * from temp_table;

输出为:
INFO
-----------------------
LogUserAConnects fired!
LogAllConnects fired!
LogUserBConnects fired!
LogAllConnects fired!
LogAllConnects fired!

9、3、1、3、3 事件属性函数
利用事件属性函数可以得到更多的事件信息,如:
Connect system/system@school AS SYSDBA

CREATE OR REPLACE TRIGGER InfoTrigger AFTER CREATE ON DATABASE
BEGIN
  insert into temp_table values(sysevent());
  insert into temp_table values(database_name());
  insert into temp_table values(login_user());
  insert into temp_table values(dictionary_obj_type());
  insert into temp_table values(dictionary_obj_name());
  insert into temp_table values(dictionary_obj_owner());
END InfoTrigger;
/

delete from temp_table;

create table stutemp
(
    sid integer primary key,
    name varchar(8) not null,
    sex char(1) check(sex in(\'M\',\'F\'))
);

select * from temp_table;

9、3、1、3、4 使用when
CREATE OR REPLACE TRIGGER InfoTrigger AFTER CREATE ON DATABASE
when (dictionary_obj_type()=\'TABLE\')
BEGIN
  insert into temp_table values(dictionary_obj_name());
  insert into temp_table values(dictionary_obj_owner());
END InfoTrigger;
/

create table stutempalso
(
    sid integer primary key,
    name varchar(8) not null,
    sex char(1) check(sex in(\'M\',\'F\'))
);

select * from temp_table;

9、3、1、3、5 补充
1)触发器可以与子程序和包、表等同名(但是不建议)
2)触发器代码中不能使用任何事务控制语句
3)可以查看数据字典了解触发器的情况,如查看用户和DBA的触发器情况:
select trigger_name,trigger_type,triggering_event from user_triggers;
select trigger_name,trigger_type,triggering_event from dba_triggers;

[此贴子已经被作者于2010-12-11 20:03:49编辑过]