第十章 PL/SQL对象类型
一、抽象的角色
抽象是对一个真实世界实体的高级描述或建模。它能排除掉无关的细节内容,使我们的日常生活更有条理。例如,驾驶一辆汽车时,我们是不需要知道它的发 动机是如何工作的。由变速排档、方向盘、加速器和刹车组成的接口就能让我们有效地使用它。而其中每一项的详细信息对于日常驾驶来说并不重要。
抽象是编程的核心内容。例如,我们在隐藏一个复杂的算法时只要编写一个过程,然后为它传递参数就可以做到过程化抽象。如果需要改变具体的实现,换一个过程体即可。有了抽象后,那些调用过程的程序就不需要再修改了。
我们在指定变量的数据类型时,可以使用数据抽象。数据类型代表了对于想操作的数值的值集合和操作符集合。比如说一个POSITIVE类型的变量,只能存放正整数,也只能用于加减乘等运算。使用这个变量时,我们不必知道PL/SQL是如何存储整数或是实现算术运算的。
对象类型是大多数编程语言内置类型的一个概括。PL/SQL提供了大量的标量类型和复合类型,每种类型都与一组预定义操作符相关联。标量类型(如 CHAR)是没有内部组成成分的。但复合类型(如RECORD)是有内部组成成分的,并且其中每一个部分都可以被独立操作。同RECORD类型一样,对象 类型也是复合类型。但是,它的操作是用户自定义的,而不是预定义的。
目前,我们还不能用PL/SQL定义对象类型。它们必须用CREATE语句创建并存放在Oracle数据库中,这样才能被许多程序所共享。使用对象 类型的程序称为客户端程序,它可以声明并操作对象,但并不要求知道对象类型是如何表现数据或实现操作。这就能够让我们分别编写程序和对象类型,即便是在改 变了对象实现时也不会影响到程序。因此,对象类型既支持过程化和又支持数据抽象。
二、什么是对象类型
对象类型是一个用户自定义复合类型,它封装了数据结构和操作这个数据结构的函数和过程。数据结构中的变量称为属性,函数和过程称为方法。通常,我们 认为对象(如人、车、银行账户)都是有着属性和行为的。例如一个婴儿有性别、年龄和体重这些属性,行为有吃、喝、睡等。对象类型能够让我们把这些内容抽象 出来并在应用程序中使用。
使用CREATE TYPE语句创建对象类型的时候,我们实际上是创建了真实世界中某个对象的抽象模板。模板只指定了我们在程序中能用到的属性和行为。比如一个雇员有很多属性,但通常只有他的一部分信息是我们的应用程序所需要的,见下图:
假设我们现在需要编写一个为雇员分发奖金的程序。因为并不是雇员的所有属性都能用于解决这个问题,所以,我们只需设计一个抽象的雇员,拥有与解决问题相关的属性即可:姓名、ID号、部门、职称、工资和级别。然后,设计一些具体的操作方法,例如更改雇员的级别。
下一步就是定义用于数据表现的变量(属性)和用于执行操作的子程序集(方法)。最后,我们把属性和方法封装到对象类型中去。
对象的属性是公有的(对客户端程序可见)。但是,设计良好的程序是不应该直接操作这些属性的,我们应该为这些操作编写相应的方法。这样,雇员数据就能保存在一个合适的状态。
在运行时,我们可以建立抽象雇员的实例(通常称为对象),然后为它的属性赋值。我们可以按照我们的需求创建任意多个实例。每个对象都有姓名、编号、职别等,如下图所示:
三、为什么使用对象类型
对象类型能把大的系统划分为多个逻辑实体,简化系统复杂度。这就使我们可以创建模块化、可维护、可重用的组件。也能让不同小组的程序员并行开发软件组件。
对象类型靠封装数据的操作来把数据维护代码从SQL脚本中分离出来,并把PL/SQL块封装到方法里。使用对象方法可以避免很多在数据访问时带来的负面影响,同时,对象类型隐藏实现细节,更新细节内容时不必修改客户端程序。
对象类型可以为现实数据建模。现实世界中的复杂实体和关系都可以直接映射到对象类型中。并且,对象类型还可以直接映射到面向对象语言(如Java和C++)的类中。
四、对象类型的结构
与包相同,对象类型也有两部分:说明和体,如下图所示。说明部分是应用程序接口;它声明了数据结构(属性集合)和所需的操作(方法)。方法体部分是对已声明方法的实现。
客户端程序要使用到的所有方法都在说明中声明。我们可以把对象说明想象成一个可选的接口,把对象体想象成一个黒盒。我们可以在不改变说明部分的前提下调试,增强或替换对象体,并且不会对客户端程序造成影响。
在一个对象说明中,所有的属性都必须声明在方法之前。只有子程序的声明才需要实现。所以,如果一个对象类型的说明只声明了属性,那么对象类型的体就没有必要了。我们不能在对象体中声明属性。对象说明中的声明都是公有的。
为了能更好的了解结构,请看下面的例子。这是一个复数的对象类型,有实数部分和虚数部分,并有几个与复数操作相关的方法。
CREATE TYPE complexAS OBJECT(
rpartREAL ,--attribute
ipartREAL ,
MEMBERFUNCTION plus(xcomplex)
RETURN complex,--method
MEMBERFUNCTION LESS(xcomplex)
RETURN complex,
MEMBERFUNCTION times(xcomplex)
RETURN complex,
MEMBERFUNCTION divby(xcomplex)
RETURN complex
);
CREATE TYPE BODY complexAS
MEMBERFUNCTION plus(xcomplex)
RETURN complexIS
BEGIN
RETURN complex(rpart+x.rpart,ipart+x.ipart);
END plus;
MEMBERFUNCTION LESS(xcomplex)
RETURN complexIS
BEGIN
RETURN complex(rpart-x.rpart,ipart-x.ipart);
END LESS;
MEMBERFUNCTION times(xcomplex)
RETURN complexIS
BEGIN
RETURN complex(rpart*x.rpart-ipart*x.ipart,
rpart*x.ipart+ipart*x.rpart);
END times;
MEMBERFUNCTION divby(xcomplex)
RETURN complexIS
zREAL :=x.rpart**2+x.ipart**2;
BEGIN
RETURN complex((rpart*x.rpart+ipart*x.ipart)/z,
(ipart*x.rpart-rpart*x.ipart)/z);
END divby;
END ;
五、对象类型组件
对象类型封装了数据和操作。我们可以在对象类型说明中声明属性和方法,但不能声明常量、异常、游标或类型。我们至少要声明一个属性(最多1000个),方法是可选的。
1、属性
同变量一样,属性也有名称和数据类型。对象类型中的名称必须是唯一的(但在其他的对象类型中可以重用)。除了下面几种类型之外,其他任何Oralce类型都可以使用:
- LONG和LONG RAW
- ROWID和UROWID
- PL/SQL特定类型BINARY_INTEGER及它的子类型、BOOLEAN、PLS_INTEGER、RECORD、REF CURSOR、%TYPE和%ROWTYPE
- PL/SQL包内定义的数据类型
我们不能在声明属性的时候用赋值语句或DEFAULT子句为它初始化。同样,也不能对属性应用NOT NULL约束。但是,对象是可以存放到添加了约束的数据表中。
数据结构中的属性集合依赖于真实世界中的对象。例如,为了表现一个分数,我们只需要两个INTEGER类型的变量。另一方面,要是表现一个学生,我们需要几个VARCHAR2来存放姓名、住址、电话号码和状态等,再添加一个VARRAY类型变量用来存储课程和分数。
数据结构可能是复杂的。例如,一个属性的数据类型可能是另外一个对象类型(称为嵌套对象类型)。有些对象类型,像队列、链表和树,都是动态的,它们是随着使用的需要而动态改变存储长度的。递归对象类型能够直接或间接的包含自身类型,这样就能创建出更诡异的数据类型。
2、方法
一般的,方法就是用关键字MEMBER或STATIC声明在对象说明部分的子程序。方法名不能和对象类型名、属性名重复。MEMBER方法只能通过对象实例调用,如:
instance_expression.method()
但是,STATIC方法直接通过对象类型调用,而不是实例,如:
object_type_name.method()
方法的定义规则与打包子程序的相同,也分为说明和体两个部分。说明部分由一个方法名和一个可选的参数列表组成,如果是函数,还需要包含一个返回类型。包体就是一段能执行一个特殊任务的代码。
对于对象类型说明中的每个方法说明,在对象类型体中都必须有与之对应的方法体实现,除非这个方法是用关键字NOT INSTANTIABLE加以限定,它的意思就是方法体的实现只在子类中出现。为了使方法说明和方法体相匹配,PL/SQL编译器采用token-by- token的方式把它们的头部进行比较。头部必须精确匹配。
与属性相同,一个形式参数的声明也是由名称和数据类型组成。但是,参数的类型不能受到大小约束。数据的类型可以是任何Oracle类型,除了那些不适用于属性的类型。这些约束也适用于返回值的类型。
- 方法实现所允许使用的语言
Oracle允许我们在PL/SQL、Java或C语言中实现对象方法。我们可以在Java或C语言中实现类型方法,只需提供一个调用说明即可。调 用说明在Oracle的数据词典中公布了Java方法或外部C函数。它把程序的名称、参数类型和返回值信息映射到对应的SQL中去。
- SELF参数
MEMBER方法接受一个内置的SELF参数,它代表了对象类型的实例。不论显式或隐式声明,它总是第一个传入MEMBER方法的参数。但是,STATIC方法就不能接受或引用SELF。
在方法体中,SELF指定了被调用方法所属的对象实例。例如,方法transform把SELF声明为IN OUT参数:
CREATE TYPE ComplexAS OBJECT(
MEMBERFUNCTION transform(SELFIN OUT Complex)...
我们不能把SELF指定成其他数据类型。在MEMBER函数中,如果SELF没有声明的话,它的参数默认为IN。但是,在MEMBER过程中,如果SELF没有什么,那么它的参数模式默认为IN OUT。并且,我们不能把SELF的模式指定为OUT。
如下例所示,方法可以直接引用SELF的属性,并不需要限定修饰词:
CREATE FUNCTION gcd(xINTEGER ,yINTEGER )
RETURN INTEGER AS
--findgreatestcommondivisorofxandy
ansINTEGER ;
BEGIN
IF (y<=x)AND (xMOD y=0)THEN
ans:=y;
ELSIF x<yTHEN
ans:=gcd(y,x);
ELSE
ans:=gcd(y,xMOD y);
END IF ;
RETURN ans;
END ;
CREATE TYPE rationalAS OBJECT(
numINTEGER ,
denINTEGER ,
MEMBERPROCEDURE normalize,
...
);
CREATE TYPE BODY rationalAS
MEMBERPROCEDURE normalizeIS
gINTEGER ;
BEGIN
g:=gcd(SELF.num,SELF.den);
g:=gcd(num,den);--equivalenttopreviousstatement
num:=num/g;
den:=den/g;
END normalize;
...
END ;
如果我们从SQL语句中调用了一个空实例(即SELF为空)的MEMBER方法,方法不会被调用,并且会返回一个空值。如果从过程语句调用的话,PL/SQL就会抛出预定义异常SELEF_IS_NULL。
- 重载
与打包子程序一样,同种类型的方法(函数或过程)都能被重载。也就是说,我们可以为不同的方法起相同的名字,只要它们的形式参数在数量、顺序或数据类型上有所不同。当我们调用其中一个方法的时候,PL/SQL会把实参列表和形参列表作比较,然后找出合适的方法。
子类型也可以重载它的基类方法。这种情况下,方法必须有完全相同的形式参数。
如果两个方法只是在参数模式上不同的话,我们是不能进行重载操作的。并且,我们不能因两个函数的返回值类型不同而对它们进行重载。
- MAP和ORDER方法
一个标量类型,如CHAR或REAL的值都有一个预定义的顺序,这样它们之间就能进行比较。但是对象类型的实例没有预定义的顺序。要想对它们进行比 较或排序就要调用我们自己实现的MAP函数。在下面的例子中,关键字MAP指明了方法convert()通过把Relational对象影射到REAL型 上,来对它们进行排序操作:
CREATE TYPE rationalAS OBJECT(
numINTEGER ,
denINTEGER ,
MAPMEMBERFUNCTION CONVERT
RETURN REAL ,
...
);
CREATE TYPE BODY rationalAS
MAPMEMBERFUNCTION CONVERT
RETURN REAL IS
BEGIN
RETURN num/den;
END CONVERT;
...
END ;
PL/SQL使用顺序来计算布尔表达式的值,如x < y,并且可以在DISTINCT,GROUP BY和ORDER BY子句中作比较。MAP方法convert()可以返回一个对象在所有Relation对象中的相对位置。
一个对象类型只能包含一个MAP方法,它接受一个内置参数SELF并返回一个标量类型:DATE、NUMBER、VARCHAR2,或是一个ANSI SQL类型,如CHARACTER或REAL。
另外,我们还可以用ORDER方法。一个对象类型只能有一个ORDER方法,它必须是一个能返回数字结果的函数。在下面的例子中,关键字ORDER表明了方法match()可以对两个对象进行比较操作:
CREATE TYPE customerAS OBJECT(
IDNUMBER ,
NAMEVARCHAR2 (20),
addrVARCHAR2 (30),
ORDER MEMBERFUNCTION match(ccustomer)
RETURN INTEGER
);
CREATE TYPE BODY customerAS
ORDER MEMBERFUNCTION match(ccustomer)
RETURN INTEGER IS
BEGIN
IF ID<c.IDTHEN
RETURN -1;--anynegativenumberwilldo
ELSIF ID>c.IDTHEN
RETURN 1;--anypositivenumberwilldo
ELSE
RETURN 0;
END IF ;
END ;
END ;
每个ORDER方法都只能接受两个参数:内置参数SELF和另外一个同类型的对象。如果c1和c2是Customer对象,一个c1 > c2这样的比较就会自动调用方法match。该方法能够返回负数、零或正数,分别代表SELF比另外一个对象小、等或大。如果传到ORDER方法的参数任 意一个为空,方法就会返回空值。
知道方针:一个MAP方法就好比一个哈希函数,能把对象值影射到标量值,然后用操作符,如<、=等来进行比较。一个ORDER方法只是简单地将两个对象进行比较。
我们可以声明一个MAP方法或是一个ORDER方法,但不同时声明这两个方法。如果我们声明了其中一个,我们就可以在SQL或过程语句中进行对象比 较。但是,如果我们没有声明方法,我们就只能在SQL语句中进行等或不等的比较。(两个同类型的对象只有它们对应的属性值相同的时候才相等。)
在对大量的对象进行排序或合并的时候,可以使用一个MAP方法。一次调用会把所有的对象影射到标量中,然后对标量值进行排序。ORDER方法的效率相对比较低,因为它必须反复地调用(它一次只能比较两个对象)。
- 构造方法
每个对象类型都有一个构造方法,它是一个名称与对象类型名称相同的函数,用于初始化,并能返回一个对象类型的新的实例。
Oracle会为每个对象类型生成一个构造函数,其中形参与对象类型的属性相匹配。也就是说,参数和属性是一一对应的关系,并且顺序、名称和数据类型都完全相同。
我们也可以定义自己的构造方法,要么覆盖掉系统定义的构造函数,要么定义一个有着不同方法签名的新构造函数。
PL/SQL从来不会隐式地调用构造函数,所以我们必须显式地调用它。
3、更改已存在对象类型的属性和方法
我们可以使用ALTER TYPE语句来添加、修改或删除属性,并可以为已存在的对象类型添加或删除方法:
CREATE TYPE person_typAS OBJECT(
NAMECHAR (20),
ssnCHAR (12),
addressVARCHAR2 (100)
);
CREATE TYPE person_ntIS TABLE OF person_typ;
CREATE TYPE dept_typAS OBJECT(
mgrperson_typ,
empsperson_nt
);
CREATE TABLE deptOF dept_typ;
--AddnewattributestoPerson_typandpropagatethechange
--toPerson_ntanddept_typ
ALTER TYPE person_typADDATTRIBUTE(pictureBLOB,dobDATE )
CASCADENOT INCLUDINGTABLE DATA;
CREATE TYPE mytypeAS OBJECT(
attr1NUMBER ,
attr2NUMBER
);
ALTER TYPE mytypeADDATTRIBUTE(attr3NUMBER ),
DROP ATTRIBUTEattr2,
ADDATTRIBUTEattr4NUMBER CASCADE;
在过程编译时,它总是使用当前引用的对象类型版本。在对象类型发生改变时,服务器端引用那个对象类型的过程就变得无效了,在下次过程被调用时它会被自动重新编译。而对于客户端引用被更改过的类型的过程,我们就必须手动编译。
如果从基类删除一个方法,那么也必须修改覆盖被删除方法的子类。我们可以用ALTER TYPE的CASADE选择来判断是否有子类被影响到;如果有子类覆盖了方法,那么语句就会被回滚。为了能成功地从基类删除一个方法,我们可以:
- 先从子类删除方法
- 从基类删除方法,然后用不带OVERRIDING关键字的ALTER TYPE把它重新添加进去
六、定义对象类型
对象类型可以表现现实世界中的任何实体。例如,一个对象类型能表现学生,银行账户,计算机显示器,有理数或者是像队列,栈,链表这样的数据结构。这一节给出了几个完整的例子,让我们了解如何设计对象类型并编写我们自己的对象类型。
目前我们还不能在PL/SQL块、子程序或包中定义对象类型。但是,我们可以在SQL*Plus中用下面的语法来定义它:
CREATE [OR REPLACE]TYPE type_name
[AUTHID {CURRENT_USER|DEFINER}]
{{IS |AS }OBJECT|UNDERsupertype_name}
(
attribute_namedatatype[,attribute_namedatatype]...
[{MAP|ORDER }MEMBERfunction_spec,]
[{FINAL|NOT FINAL}MEMBERfunction_spec,]
[{INSTANTIABLE|NOT INSTANTIABLE}MEMBERfunction_spec,]
[{MEMBER|STATIC}{subprogram_spec|call_spec}
[,{MEMBER|STATIC}{subprogram_spec|call_spec}]...]
)[{FINAL|NOT FINAL}][{INSTANTIABLE|NOT INSTANTIABLE}];
[CREATE [OR REPLACE]TYPE BODY type_name{IS |AS }
{{MAP|ORDER }MEMBERfunction_body;
|{MEMBER|STATIC}{subprogram_body|call_spec};}
[{MEMBER|STATIC}{subprogram_body|call_spec};]...
END ;]
1、PL/SQL类型继承一览
PL/SQL支持单继承模式。我们可以定义对象类型的子类型。这些子类型包括父类型(或超类)所有的属性和方法。子类型还可以包括额外的属性和方法,并可以覆盖超类的方法。
我们还可以定义子类是否能继承于某个特定的类型。我们也可以定义不能直接初始化的类型和方法,只有声明它们的子类才可以进行初始化操作。
有些类型属性可以用ALTER TYPE语句动态的改变。当基类发生变化时,无论是用ALTER TYPE语句还是重新定义基类,子类会自动的应用这些改变的内容。我们可以用TREAT操作符只返回某一个指定的子类的对象。
从REF和DEREF函数中产生的值可以代表声明过的表或视图类型,或是一个或多个它的子类型。
- PL/SQL类继承举例
--Createasupertypefromwhichseveralsubtypeswillbederived.
CREATE TYPE person_typAS OBJECT(
ssnNUMBER ,
NAMEVARCHAR2 (30),
addressVARCHAR2 (100)
)
NOT FINAL;
--Deriveasubtypethathasalltheattributesofthesupertype,
--plussomeadditionalattributes.
CREATE TYPE student_typUNDERperson_typ(
deptidNUMBER ,
majorVARCHAR2 (30)
)
NOT FINAL;
--BecauseStudent_typisdeclaredNOTFINAL,youcanderive
--furthersubtypesfromit.
CREATE TYPE parttimestudent_typUNDERstudent_typ(
numhoursNUMBER
)
;
--Deriveanothersubtype.Becauseithasthedefaultattribute
--FINAL,youcannotuseEmployee_typasasupertypeandderive
--subtypesfromit.
CREATE TYPE employee_typUNDERperson_typ(
empidNUMBER ,
mgrVARCHAR2 (30)
)
;
--Defineanobjecttypethatcanbeasupertype.Becausethe
--memberfunctionisFINAL,itcannotbeoverriddeninany
--subtypes.
CREATE TYPE TAS OBJECT(...,MEMBERPROCEDURE Print(),FINALMEMBER
FUNCTION foo(xNUMBER )...)NOT FINAL;
--Weneverwanttocreateanobjectofthissupertype.Byusing
--NOTINSTANTIABLE,weforceallobjectstouseoneofthesubtypes
--instead,withspecificimplementationsforthememberfunctions.
CREATE TYPE Address_typAS OBJECT(...)NOT INSTANTIABLENOT FINAL;
--Thesesubtypescanprovidetheirownimplementationsof
--memberfunctions,suchasforvalidatingphonenumbersand
--postalcodes.Becausethereisno"generic"wayofdoingthese
--things,onlyobjectsofthesesubtypescanbeinstantiated.
CREATE TYPE USAddress_typUNDERAddress_typ(...);
CREATE TYPE IntlAddress_typUNDERAddress_typ(...);
--ReturnREFsforthosePerson_typobjectsthatareinstancesof
--theStudent_typsubtype,andNULL REFsotherwise.
SELECT TREAT(REF (p)AS REF student_typ)
FROM person_vp;
--ExampleofusingTREATforassignment...
--ReturnREFsforthosePerson_typeobjectsthatareinstancesof
--Employee_typeorStudent_typ,oranyoftheirsubtypes.
SELECT REF (p)
FROM person_vp
WHERE VALUE(p)IS OF (employee_typ,student_typ);
--Similartoabove,butdonotallowanysubtypesofStudent_typ.
SELECT REF (p)
FROM person_vp
WHERE VALUE(p)IS OF (ONLYstudent_typ);
--TheresultsofREFandDEREFcanincludeobjectsofPerson_typ
--anditssubtypessuchasEmployee_typandStudent_typ.
SELECT REF (p)
FROM person_vp;
SELECT DEREF(REF (p))
FROM person_vp;
2、对象类型实例:栈
栈是一个有序集合。栈有一个栈顶和一个栈底。栈中的每一项都只能在栈顶添加或删除。所以,最后一个被加入栈的项会被最先删除。(可以把栈想象成自助餐厅中的盘子。)压栈和退栈操作能够对栈进行后进先出(LIFO)更新。
栈能应用在很多地方。例如,它们可以用在系统编程中控制中断优先级并对递归进行管理。最简单的栈实现就是使用整数数组,数组的一端代表了栈顶。
PL/SQL提供了VARRAY数据类型,它能让我们声明变长数组。要声明变长数组属性,必须先定义变长数组类型。但是,我们不能再对象说明中定义类型,所以,我们只能单独的定义变长数组类型,并指定它的最大长度,具体实现如下:
CREATE TYPE IntArrayAS VARRAY(25)OF INTEGER ;
现在我们可以编写对象类型说明了:
CREATE TYPE stackAS OBJECT(
max_sizeINTEGER ,
topINTEGER ,
POSITIONintarray,
MEMBERPROCEDURE initialize,
MEMBERFUNCTION FULL
RETURN BOOLEAN ,
MEMBERFUNCTION empty
RETURN BOOLEAN ,
MEMBERPROCEDURE push(nIN INTEGER ),
MEMBERPROCEDURE pop(nOUT INTEGER )
);
最后,我们可以编写对象类型体:
CREATE TYPE BODY stackAS
MEMBERPROCEDURE initializeIS
BEGIN
top:=0;
/*Callconstructorforvarrayandsetelement1toNULL.*/
POSITION:=intarray(NULL );
max_size:=POSITION.LIMIT;--getvarraysizeconstraint
POSITION.EXTEND(max_size-1,1);--copyelement1into2..25
END initialize;
MEMBERFUNCTION FULL
RETURN BOOLEAN IS
BEGIN
RETURN (top=max_size);--returnTRUEifstackisfull
END FULL;
MEMBERFUNCTION empty
RETURN BOOLEAN IS
BEGIN
RETURN (top=0);--returnTRUEifstackisempty
END empty;
MEMBERPROCEDURE push(nIN INTEGER )IS
BEGIN
IF NOT FULLTHEN
top:=top+1;--pushintegerontostack
POSITION(top):=n;
ELSE --stackisfull
raise_application_error(-20101,'stackoverflow' );
END IF ;
END push;
MEMBERPROCEDURE pop(nOUT INTEGER )IS
BEGIN
IF NOT emptyTHEN
n:=POSITION(top);
top:=top-1;--popintegeroffstack
ELSE
--stackisempty
raise_application_error(-20102,'stackunderflow' );
END IF ;
END pop;
END ;
在成员过程push和pop中,我们使用内置过程raise_application_error来关联用户定义的错误消息。这样,我们就能把错误 报告给客户端程序而避免把未控制异常传给主环境。客户端程序捕获PL/SQL异常后,可以在OTHERS异常控制句柄中用SQLCODE和SQLERRM 来确定具体的错误信息。下例中,当异常被抛出时,我们就把对应的错误消息输出:
DECLARE
...
BEGIN
...
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line(SQLERRM );
END ;
另外,程序还可以使用编译指示EXCEPTION_INIT把raise_application_error返回的错误编号影射到命名异常中,如下例所示:
DECLARE
stack_overflowEXCEPTION ;
stack_underflowEXCEPTION ;
PRAGMA EXCEPTION_INIT(stack_overflow,-20101);
PRAGMA EXCEPTION_INIT(stack_underflow,-20102);
BEGIN
...
EXCEPTION
WHEN stack_overflowTHEN
...
END ;
3、对象类型实例:售票处
假如有一个连锁电影院,每个影院有三个银幕。每个影院有一个售票处,销售三种不同电影的影票。所有影票的价格都为三美元。定期检查影票的销售情况,然后及时补充影票。
在定义代表销售处的对象类型之前,我们必须考虑到必要的数据和操作。对于一个简单的售票处来说,对象类型需要票价、当前影票存量和已售影票的收据这些属性。它还需要一些方法:购票、盘存、补充存量和收集收据。
对于收据,我们可以使用含有三个元素的数组。元素1、2和3各自记录电影1、2和3。要声明一个变长数组属性,我们就必须先像下面这样定义它的类型:
CREATE TYPE RealArrayAS VARRAY(3)OF REAL ;
现在,我们可以编写对象类型说明:
CREATE TYPE ticket_boothAS OBJECT(
priceREAL ,
qty_on_handINTEGER ,
receiptsrealarray,
MEMBERPROCEDURE initialize,
MEMBERPROCEDURE purchase(movieINTEGER ,amountREAL ,CHANGEOUT REAL ),
MEMBERFUNCTION inventory
RETURN INTEGER ,
MEMBERPROCEDURE replenish(quantityINTEGER ),
MEMBERPROCEDURE COLLECT (movieINTEGER ,amountOUT REAL )
);
最后,我们可以编写对象类型体:
CREATE TYPE BODY ticket_boothAS
MEMBERPROCEDURE initializeIS
BEGIN
price:=3.00;
qty_on_hand:=5000;--provideinitialstockoftickets
--callconstructorforvarrayandsetelements1..3tozero
receipts:=realarray(0,0,0);
END initialize;
MEMBERPROCEDURE purchase(movieINTEGER ,amountREAL ,CHANGEOUT REAL )IS
BEGIN
IF qty_on_hand=0THEN
raise_application_error(-20103,'outofstock' );
END IF ;
IF amount>=priceTHEN
qty_on_hand:=qty_on_hand-1;
receipts(movie):=receipts(movie)+price;
CHANGE:=amount-price;
ELSE --amountisnotenough
CHANGE:=amount;--soreturnfullamount
END IF ;
END purchase;
MEMBERFUNCTION inventory
RETURN INTEGER IS
BEGIN
RETURN qty_on_hand;
END inventory;
MEMBERPROCEDURE replenish(quantityINTEGER )IS
BEGIN
qty_on_hand:=qty_on_hand+quantity;
END replenish;
MEMBERPROCEDURE COLLECT (movieINTEGER ,amountOUT REAL )IS
BEGIN
amount:=receipts(movie);--getreceiptsforagivenmovie
receipts(movie):=0;--resetreceiptstozero
END COLLECT ;
END ;
4、对象类型实例:银行账户
在定义银行账户对象类型之前,我们必须考虑一下要使用的数据和操作。对于一个简单的银行账户来说,对象类型需要一个账号、余额和状态这三个属性。所需的操作有:打开帐户,验证账号,关闭账户,存款,取款和余额结算。
首先,我们要像下面这样编写对象类型说明:
CREATE TYPE bank_accountAS OBJECT(
acct_numberINTEGER (5),
balanceREAL ,
statusVARCHAR2 (10),
MEMBERPROCEDURE OPEN (amountIN REAL ),
MEMBERPROCEDURE verify_acct(numIN INTEGER ),
MEMBERPROCEDURE CLOSE (numIN INTEGER ,amountOUT REAL ),
MEMBERPROCEDURE deposit(numIN INTEGER ,amountIN REAL ),
MEMBERPROCEDURE withdraw(numIN INTEGER ,amountIN REAL ),
MEMBERFUNCTION curr_bal(numIN INTEGER )
RETURN REAL
);
然后编写对象体:
CREATE TYPE BODY bank_accountAS
MEMBERPROCEDURE OPEN (amountIN REAL )IS
--openaccountwithinitialdeposit
BEGIN
IF NOT amount>0THEN
raise_application_error(-20104,'badamount' );
END IF ;
SELECT acct_sequence.NEXTVAL
INTO acct_number
FROM DUAL;
status:='open' ;
balance:=amount;
END OPEN ;
MEMBERPROCEDURE verify_acct(numIN INTEGER )IS
--checkforwrongaccountnumberorclosedaccount
BEGIN
IF (num<>acct_number)THEN
raise_application_error(-20105,'wrongnumber' );
ELSIF (status='closed' )THEN
raise_application_error(-20106,'accountclosed' );
END IF ;
END verify_acct;
MEMBERPROCEDURE CLOSE (numIN INTEGER ,amountOUT REAL )IS
--closeaccountandreturnbalance
BEGIN
verify_acct(num);
status:='closed' ;
amount:=balance;
END CLOSE ;
MEMBERPROCEDURE deposit(numIN INTEGER ,amountIN REAL )IS
BEGIN
verify_acct(num);
IF NOT amount>0THEN
raise_application_error(-20104,'badamount' );
END IF ;
balance:=balance+amount;
END deposit;
MEMBERPROCEDURE withdraw(numIN INTEGER ,amountIN REAL )IS
--ifaccounthasenoughfunds,withdraw
--givenamount;else,raiseanexception
BEGIN
verify_acct(num);
IF amount<=balanceTHEN
balance:=balance-amount;
ELSE
raise_application_error(-20107,'insufficientfunds' );
END IF ;
END withdraw;
MEMBERFUNCTION curr_bal(numIN INTEGER )
RETURN REAL IS
BEGIN
verify_acct(num);
RETURN balance;
END curr_bal;
END ;
5、对象类型实例:实数
有理数能够表现成两个整数相除的形式,一个分子和一个分母。同大多数语言一样,PL/SQL并没有实数类型或是用于实数操作的预定义操作符。现在让我们就用对象类型来弥补这个缺失。首先,编写下面的对象说明:
CREATE TYPE rationalAS OBJECT(
numINTEGER ,
denINTEGER ,
MAPMEMBERFUNCTION CONVERT
RETURN REAL ,
MEMBERPROCEDURE normalize,
MEMBERFUNCTION reciprocal
RETURN rational,
MEMBERFUNCTION plus(xrational)
RETURN rational,
MEMBERFUNCTION LESS(xrational)
RETURN rational,
MEMBERFUNCTION times(xrational)
RETURN rational,
MEMBERFUNCTION divby(xrational)
RETURN rational,
PRAGMA RESTRICT_REFERENCES(DEFAULT ,RNDS,WNDS,RNPS,WNPS)
);
PL/SQL不允许操作符重载。所以我们必须定义方法plus(),less()(minus是保留关键字),times()和divby()来替代操作符+、-、*和/。
下一步,创建下面的独立存储函数,它们会被方法normalize()调用:
CREATE FUNCTION gcd(xINTEGER ,yINTEGER )
RETURN INTEGER AS
--findgreatestcommondivisorofxandy
ansINTEGER ;
BEGIN
IF (y<=x)AND (xMOD y=0)THEN
ans:=y;
ELSIF x<yTHEN
ans:=gcd(y,x);--recursivecall
ELSE
ans:=gcd(y,xMOD y);--recursivecall
END IF ;
RETURN ans;
END ;
下面是对象类型体的内容:
CREATE TYPE BODY rationalAS
MAPMEMBERFUNCTION CONVERT
RETURN REAL IS
--convertrationalnumbertorealnumber
BEGIN
RETURN num/den;
END CONVERT;
MEMBERPROCEDURE normalizeIS
--reducefractionnum/dentolowestterms
gINTEGER ;
BEGIN
g:=gcd(num,den);
num:=num/g;
den:=den/g;
END normalize;
MEMBERFUNCTION reciprocal
RETURN rationalIS
--returnreciprocalofnum/den
BEGIN
RETURN rational(den,num);--callconstructor
END reciprocal;
MEMBERFUNCTION plus(xrational)
RETURN rationalIS
--returnsumofSELF+x
rrational;
BEGIN
r:=rational(num*x.den+x.num*den,den*x.den);
r.normalize;
RETURN r;
END plus;
MEMBERFUNCTION LESS(xrational)
RETURN rationalIS
--returndifferenceofSELF-x
rrational;
BEGIN
r:=rational(num*x.den-x.num*den,den*x.den);
r.normalize;
RETURN r;
END LESS;
MEMBERFUNCTION times(xrational)
RETURN rationalIS
--returnproductofSELF*x
rrational;
BEGIN
r:=rational(num*x.num,den*x.den);
r.normalize;
RETURN r;
END times;
MEMBERFUNCTION divby(xrational)
RETURN rationalIS
--returnquotientofSELF/x
rrational;
BEGIN
r:=rational(num*x.den,den*x.num);
r.normalize;
RETURN r;
END divby;
END ;
七、声明并初始化对象
只要对象类型在模式中定义了,我们就可以在任何PL/SQL块、子程序或包中引用它来声明对象。例如,我们可以使用对象类型作为属性、字段、变量、 绑定变量、记录的域、表元的素、形式参数或函数返回值的数据类型。在运行时,对象类型的实例会被创建,也就是对象实例被初始化。
1、定义对象
我们可以在使用内置类型(如CHAR或NUMBER)的地方使用对象类型。在下面的块中,我们声明了Rational类型的对象r。然后调用构造函数初始化对象,把值6和8分别赋给属性num和den。
DECLARE
rrational;
BEGIN
r:=rational(6,8);
DBMS_OUTPUT.put_line(r.num);--prints6
END ;
我们还可以把对象作为函数和过程的形式参数。那样就能把对象从一个子程序传递到另一个子程序了。在下面的例子中,我们使用对象类型Account作为形式参数:
DECLARE
...
PROCEDURE open_acct(new_acctIN OUT Account)IS ...
下面,我们把Accout作为函数的返回类型来使用:
DECLARE
...
FUNCTION get_acct(acct_idIN INTEGER )RETURN AccountIS ...
2、初始化对象
如果不调用构造函数初始化对象,那它会被自动赋上空值。也就是说对象本身为空,不单单指它的属性。如下例:
DECLARE
rrational;--rbecomesatomicallynull
BEGIN
r:=rational(2,3);--rbecomes2/3
END ;
一个空对象总不能等于另一个对象。实际上,拿一个空对象与另一个对象比较结果总是NULL.同样,如果把一个空对象或NULL赋给另一个对象,那么被赋值的对象也为空,示例如下:
DECLARE
rrational;
BEGIN
r:=rational(1,2);--rbecomes1/2
r:=NULL ;--rbecomesatomicallynull
IF rIS NULL THEN ...--conditionyieldsTRUE
一个好的编程习惯就是在声明的时候就为对象初始化,例如:
DECLARE
rRational:=Rational(2,3);--rbecomes2/3
3、PL/SQL如何对待未初始化对象
在表达式中,未初始化的对象属性值为NULL。如果为一个未初始化的对象属性赋值,就会引起预定义异常ACCESS_INTO_NULL。当对未初始化的对象或是它的属性使用IS NULL做比较时,结果总为TRUE。
下面的例子就演示了空对象和含有空属性的对象之间的差异:
DECLARE
rrational;--risatomicallynull
BEGIN
IF rIS NULL THEN ...--yieldsTRUE
IF r.numIS NULL THEN ...--yieldsTRUE
r:=rational(NULL ,NULL );--initializesr
r.num:=4;--succeedsbecauserisnolongeratomicallynull
--eventhoughallitsattributesarenull
r:=NULL ;--rbecomesatomicallynullagain
r.num:=4;--raisesACCESS_INTO_NULL
EXCEPTION
WHEN ACCESS_INTO_NULLTHEN
...;
END ;
调用一个未初始化对象的方法会引起预定义异常NULL_SELF_DISPATCH。如果把未初始化对象的属性作为IN模式参数进行传递时,就跟传递一个NULL参数一样;如果把它作为OUT或IN OUT模式参数传递,并且尝试为其赋值,就会引起异常。
八、访问属性
我们只能按名称来引用属性,不可以使用它的位置来进行引用。在下面的例子中,我们先把属性den赋给变量denominator,然后把变量numerator的值赋给属性num。
DECLARE
rrational:=rational(NULL ,NULL );
numeratorINTEGER ;
denominatorINTEGER ;
BEGIN
...
denominator:=r.den;
r.num:=numerator;
END ;
下面再看一个稍微复杂一点的对象嵌套例子:
CREATE TYPE addressAS OBJECT(
streetVARCHAR2 (30),
cityVARCHAR2 (20),
stateCHAR (2),
zip_codeVARCHAR2 (5)
);
CREATE TYPE studentAS OBJECT(
NAMEVARCHAR2 (20),
home_addressaddress,
phone_numberVARCHAR2 (10),
statusvarcahr2(10),
advisor_nameVARCHAR2 (20),
...
);
这里要注意的是,zip_code是对象类型Address的一个属性,而Address又是对象Student的属性home_address的数据类型。如果s是Student的对象的话,我们就可以像下面这样访问它的zip_code属性:
s.home_address.zip_code
九、定义构造函数
默认情况下,我们不需要为对象类型定义构造函数,因为系统会提供一个接受与每个属性相对应的参数的构造函数。
我们也许想定义自己的构造函数:
- 为某些属性提供默认值,这样就能确保属性值的正确性而不必依赖于调用者所提供的每一个属性值。
- 避免许多特殊用途的过程只初始化对象的不同部分。
- 当新的属性加到对象类型中时,避免更改调用构造函数的应用程序中的代码。构造函数也许需要一些新的代码,例如把属性设置为空,但我们还需要保持方法签名不变,这样可以使已存在的构造函数调用继续工作。
CREATE OR REPLACETYPE rectangleAS OBJECT(
--Thetypehas3attributes.
LENGTHNUMBER ,
widthNUMBER ,
areaNUMBER ,
--Defineaconstructorthathasonly2parameters.
CONSTRUCTORFUNCTION rectangle(LENGTHNUMBER ,widthNUMBER )
RETURN SELFAS RESULT
);
/
CREATE OR REPLACETYPE BODY rectangleAS
CONSTRUCTORFUNCTION rectangle(LENGTHNUMBER ,widthNUMBER )
RETURN SELFAS RESULTAS
BEGIN
SELF.LENGTH:=LENGTH;
SELF.width:=width;
--Wecomputethearearatherthanacceptingitasaparameter.
SELF.area:=LENGTH*width;
RETURN ;
END ;
END ;
/
DECLARE
r1rectangle;
r2rectangle;
BEGIN
--Wecanstillcallthedefaultconstructor,withall3parameters.
r1:=NEW rectangle(10,20,200);
--Butitismorerobusttocallourconstructor,whichcomputes
--theAREAattribute.ThisguaranteesthattheinitialvalueisOK.
r2:=NEW rectangle(10,20);
END ;
/
十、调用构造函数
只要是能够调用普通函数的地方,我们就可以调用构造函数。跟所有的函数一样,构造函数也可以作为表达式的一部分而被调用,如下例所示:
DECLARE
r1rational:=rational(2,3);
FUNCTION average(xrational,yrational)
RETURN rationalIS
BEGIN
...
END ;
BEGIN
r1:=average(rational(3,4),rational(7,11));
IF (rational(5,8)>r1)THEN
...
END IF ;
END ;
当我们为构造函数传递参数的时候,调用会把对象中需要初始化的属性赋上初始值。如果是调用默认的构造函数,我们就需要为每个属性指定一个初始值;跟常量和变量不同,属性是不可以有默认值的。下例中,第n个参数为第n个属性赋值:
DECLARE
rrational;
BEGIN
r:=rational(5,6);--assign5tonum,6toden
--nowris5/6
十一、调用方法
跟打包子程序一样,方法也是使用点标志来调用的。示例如下:
DECLARE
rrational;
BEGIN
r:=rational(6,8);
r.normalize;
DBMS_OUTPUT.put_line(r.num);--prints3
END ;
如下例所示,我们可以连续调用方法。执行顺序从左到右。首先,成员函数reciprocal()会被调用,然后成员过程normalize()被调用。
DECLARE
rrational:=rational(6,8);
BEGIN
r.reciprocal().normalize;
DBMS_OUTPUT.put_line(r.num);--prints4
END ;
在SQL语句中,调用无参数的方法需要使用一个空的参数列表。在过程化语句中,空的参数列表是可选的,除非我们使用链式调用,这时除了最后一个调用之外其他的都需要空的参数列表。
我们不能把过程作为连续调用的一部分,因为过程是作为语句使用而不是表达式。所以,像下面这样的语句是不允许的:
r.normalize().reciprocal;--notallowed
同样,如果连续调用两个函数,第一个函数必须返回一个能传入第二个函数的对象。对于静态方法,我们使用下面的语法:
type_name.method_name
这种调用是不需要对象实例的。从子类实例中调用方法时,实际执行的方法是由类的继承关系决定的。如果子类覆盖了基类的方法,子类的方法就会被调用;否则的话,基类的方法会被调用。这种情况称为动态方法分派。
十二、通过REF修饰符共享对象
在真实世界中的大多数对象都要比有理数类型庞大而且复杂。如果对象比较大的话,把对象副本从一个子程序传递到另一个子程序时效率就可能会很低。这时如果使用对象共享就很有意义了,我们可以使用一个指向对象的引用来引用所需要的对象。
共享有两个重要的好处。首先,避免了不必要的数据重复。其次,在共享的对象内容更新时,任何引用所指向的内容也会被立即更新。如下面的例子:
CREATE TYPE homeAS OBJECT(
addressVARCHAR2 (35),
ownerVARCHAR2 (25),
ageINTEGER ,
styleVARCHAR (15),
floor_planBLOB,
priceREAL (9,2),
...
);
/
CREATE TABLE homesOF home;
修改一下Person,我们就能建立家庭的模型,几个人共享一个家。我们可以使用修饰符REF来声明引用:
CREATE TYPE personAS OBJECT(
first_name