第十章 PL/SQL对象类型

本文介绍了PL/SQL中的对象类型概念,包括抽象角色、对象类型定义与使用、构造方法及属性方法等内容,并通过栈、售票处和银行账户等实例展示了对象类型的应用。
<style type="text/css"> /* banner */ #banner { position:static; top: 0px; left: 0px; width: 100%; height: 98px; background-repeat:no-repeat; background-image: url(../images/banner.gif); background-position:right; } /* book name */ #book { font-family: "Times New Roman", Times, serif; font-size: 42px; font-weight: bold; padding: 10px; text-align: center; } /* chapter title */ #chapter { font-family: "幼圆", Georgia, "Times New Roman", Times, serif; font-size: 36px; font-weight: bold; padding: 10px; text-align: center; } /* text */ #text { padding: 10px; font-family: "宋体", "SimSun", sans-serif, "Times New Roman", "Palatino Linotype", Georgia; font-size: 16px; line-height: 24px; text-indent: 32px; } /* body layout */ body { padding-top: 20px; padding-right: 6%; padding-bottom: 20px; padding-left: 6%; background-color: #FFFFEE; background-image: url(../images/cr.gif); background-repeat: repeat; background-attachment: fixed; } /* titles */ .title1 { font-family: "Palatino Linotype", Georgia, sans-serif, "宋体"; font-size: 24px; font-weight: bold; } .title2 { font-family: "Palatino Linotype", Georgia, sans-serif, "宋体"; font-size: 20px; font-weight: bold; } /* subtitle */ ol li ol li{ list-style-type: decimal; margin-left: 8px; line-height: 24px; text-indent: 0px; } /* code style */ blockquote table tr td { padding: 10px; font-family: "Courier New"; font-size: 15px; text-indent: 0px; color: #003366; background-color: #FBFEFF; border: 1px dotted #000000; line-height: 18px; } /* for keyword */ strong { color: #000066; } /* for string */ em { color: #0000FF; } /* for comments */ i{ color: #3F7F5F; } /* illustration */ img { margin-left: 45px; padding-left: 15px; padding: 15px; } /* table list */ #table-list { margin-left: 45px; font-family: "Palatino Linotype", Georgia, sans-serif, "宋体"; font-size: 14px; text-indent: 0px; border-top-width: 3px; border-bottom-width: 3px; border-top-style: solid; border-bottom-style: solid; border-top-color: #000000; border-bottom-color: #000000; } #table-list-head { padding: 5px; text-indent: 0px; background-color: #000000; color: #FFFFFF; }</style>

第十章 PL/SQL对象类型
<!-- InstanceEndEditable --><!-- InstanceBeginEditable name="EditRegion2" -->

一、抽象的角色

抽象是对一个真实世界实体的高级描述或建模。它能排除掉无关的细节内容,使我们的日常生活更有条理。例如,驾驶一辆汽车时,我们是不需要知道它的发动机是如何工作的。由变速排档、方向盘、加速器和刹车组成的接口就能让我们有效地使用它。而其中每一项的详细信息对于日常驾驶来说并不重要。

抽象是编程的核心内容。例如,我们在隐藏一个复杂的算法时只要编写一个过程,然后为它传递参数就可以做到过程化抽象。如果需要改变具体的实现,换一个过程体即可。有了抽象后,那些调用过程的程序就不需要再修改了。

我们在指定变量的数据类型时,可以使用数据抽象。数据类型代表了对于想操作的数值的值集合和操作符集合。比如说一个POSITIVE类型的变量,只能存放正整数,也只能用于加减乘等运算。使用这个变量时,我们不必知道PL/SQL是如何存储整数或是实现算术运算的。

对象类型是大多数编程语言内置类型的一个概括。PL/SQL提供了大量的标量类型和复合类型,每种类型都与一组预定义操作符相关联。标量类型(如CHAR)是没有内部组成成分的。但复合类型(如RECORD)是有内部组成成分的,并且其中每一个部分都可以被独立操作。同RECORD类型一样,对象类型也是复合类型。但是,它的操作是用户自定义的,而不是预定义的。

目前,我们还不能用PL/SQL定义对象类型。它们必须用CREATE语句创建并存放在Oracle数据库中,这样才能被许多程序所共享。使用对象类型的程序称为客户端程序,它可以声明并操作对象,但并不要求知道对象类型是如何表现数据或实现操作。这就能够让我们分别编写程序和对象类型,即便是在改变了对象实现时也不会影响到程序。因此,对象类型既支持过程化和又支持数据抽象。

二、什么是对象类型

对象类型是一个用户自定义复合类型,它封装了数据结构和操作这个数据结构的函数和过程。数据结构中的变量称为属性,函数和过程称为方法。通常,我们认为对象(如人、车、银行账户)都是有着属性和行为的。例如一个婴儿有性别、年龄和体重这些属性,行为有吃、喝、睡等。对象类型能够让我们把这些内容抽象出来并在应用程序中使用。

使用CREATE TYPE语句创建对象类型的时候,我们实际上是创建了真实世界中某个对象的抽象模板。模板只指定了我们在程序中能用到的属性和行为。比如一个雇员有很多属性,但通常只有他的一部分信息是我们的应用程序所需要的,见下图:

假设我们现在需要编写一个为雇员分发奖金的程序。因为并不是雇员的所有属性都能用于解决这个问题,所以,我们只需设计一个抽象的雇员,拥有与解决问题相关的属性即可:姓名、ID号、部门、职称、工资和级别。然后,设计一些具体的操作方法,例如更改雇员的级别。

下一步就是定义用于数据表现的变量(属性)和用于执行操作的子程序集(方法)。最后,我们把属性和方法封装到对象类型中去。

对象的属性是公有的(对客户端程序可见)。但是,设计良好的程序是不应该直接操作这些属性的,我们应该为这些操作编写相应的方法。这样,雇员数据就能保存在一个合适的状态。

在运行时,我们可以建立抽象雇员的实例(通常称为对象),然后为它的属性赋值。我们可以按照我们的需求创建任意多个实例。每个对象都有姓名、编号、职别等,如下图所示:

三、为什么使用对象类型

对象类型能把大的系统划分为多个逻辑实体,简化系统复杂度。这就使我们可以创建模块化、可维护、可重用的组件。也能让不同小组的程序员并行开发软件组件。

对象类型靠封装数据的操作来把数据维护代码从SQL脚本中分离出来,并把PL/SQL块封装到方法里。使用对象方法可以避免很多在数据访问时带来的负面影响,同时,对象类型隐藏实现细节,更新细节内容时不必修改客户端程序。

对象类型可以为现实数据建模。现实世界中的复杂实体和关系都可以直接映射到对象类型中。并且,对象类型还可以直接映射到面向对象语言(如Java和C++)的类中。

四、对象类型的结构

与包相同,对象类型也有两部分:说明和体,如下图所示。说明部分是应用程序接口;它声明了数据结构(属性集合)和所需的操作(方法)。方法体部分是对已声明方法的实现。

客户端程序要使用到的所有方法都在说明中声明。我们可以把对象说明想象成一个可选的接口,把对象体想象成一个黒盒。我们可以在不改变说明部分的前提下调试,增强或替换对象体,并且不会对客户端程序造成影响。

在一个对象说明中,所有的属性都必须声明在方法之前。只有子程序的声明才需要实现。所以,如果一个对象类型的说明只声明了属性,那么对象类型的体就没有必要了。我们不能在对象体中声明属性。对象说明中的声明都是公有的。

为了能更好的了解结构,请看下面的例子。这是一个复数的对象类型,有实数部分和虚数部分,并有几个与复数操作相关的方法。

CREATETYPEcomplexASOBJECT(
rpartREAL,--attribute
ipartREAL,
MEMBERFUNCTIONplus(xcomplex)
RETURNcomplex,--method
MEMBERFUNCTIONLESS(xcomplex)
RETURNcomplex,
MEMBERFUNCTIONtimes(xcomplex)
RETURNcomplex,
MEMBERFUNCTIONdivby(xcomplex)
RETURNcomplex
);

CREATETYPEBODYcomplexAS

MEMBERFUNCTIONplus(xcomplex)
RETURNcomplexIS
BEGIN
RETURNcomplex(rpart+x.rpart,ipart+x.ipart);
ENDplus;

MEMBERFUNCTIONLESS(xcomplex)
RETURNcomplexIS
BEGIN
RETURNcomplex(rpart-x.rpart,ipart-x.ipart);
ENDLESS;

MEMBERFUNCTIONtimes(xcomplex)
RETURNcomplexIS
BEGIN
RETURNcomplex(rpart*x.rpart-ipart*x.ipart,
rpart*x.ipart+ipart*x.rpart);
ENDtimes;

MEMBERFUNCTIONdivby(xcomplex)
RETURNcomplexIS
zREAL:=x.rpart**2+x.ipart**2;
BEGIN
RETURNcomplex((rpart*x.rpart+ipart*x.ipart)/z,
(ipart*x.rpart-rpart*x.ipart)/z);
ENDdivby;
END;

五、对象类型组件

对象类型封装了数据和操作。我们可以在对象类型说明中声明属性和方法,但不能声明常量、异常、游标或类型。我们至少要声明一个属性(最多1000个),方法是可选的。

1、属性

同变量一样,属性也有名称和数据类型。对象类型中的名称必须是唯一的(但在其他的对象类型中可以重用)。除了下面几种类型之外,其他任何Oralce类型都可以使用:

  1. LONG和LONG RAW
  2. ROWID和UROWID
  3. PL/SQL特定类型BINARY_INTEGER及它的子类型、BOOLEAN、PLS_INTEGER、RECORD、REF CURSOR、%TYPE和%ROWTYPE
  4. 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参数:

CREATETYPEComplexASOBJECT(
MEMBERFUNCTIONtransform(SELFINOUTComplex)...

我们不能把SELF指定成其他数据类型。在MEMBER函数中,如果SELF没有声明的话,它的参数默认为IN。但是,在MEMBER过程中,如果SELF没有什么,那么它的参数模式默认为IN OUT。并且,我们不能把SELF的模式指定为OUT。

如下例所示,方法可以直接引用SELF的属性,并不需要限定修饰词:

CREATEFUNCTIONgcd(xINTEGER,yINTEGER)
RETURNINTEGERAS
--findgreatestcommondivisorofxandy
ansINTEGER;
BEGIN
IF(y<=x)AND(xMODy=0)THEN
ans:=y;
ELSIFx<yTHEN
ans:=gcd(y,x);
ELSE
ans:=gcd(y,xMODy);
ENDIF;

RETURNans;
END;

CREATETYPErationalASOBJECT(
numINTEGER,
denINTEGER,
MEMBERPROCEDUREnormalize,
...
);

CREATETYPEBODYrationalAS
MEMBERPROCEDUREnormalizeIS
gINTEGER;
BEGIN
g:=gcd(SELF.num,SELF.den);
g:=gcd(num,den);--equivalenttopreviousstatement
num:=num/g;
den:=den/g;
ENDnormalize;
...
END;

如果我们从SQL语句中调用了一个空实例(即SELF为空)的MEMBER方法,方法不会被调用,并且会返回一个空值。如果从过程语句调用的话,PL/SQL就会抛出预定义异常SELEF_IS_NULL。

  • 重载

与打包子程序一样,同种类型的方法(函数或过程)都能被重载。也就是说,我们可以为不同的方法起相同的名字,只要它们的形式参数在数量、顺序或数据类型上有所不同。当我们调用其中一个方法的时候,PL/SQL会把实参列表和形参列表作比较,然后找出合适的方法。

子类型也可以重载它的基类方法。这种情况下,方法必须有完全相同的形式参数。

如果两个方法只是在参数模式上不同的话,我们是不能进行重载操作的。并且,我们不能因两个函数的返回值类型不同而对它们进行重载。

  • MAP和ORDER方法

一个标量类型,如CHAR或REAL的值都有一个预定义的顺序,这样它们之间就能进行比较。但是对象类型的实例没有预定义的顺序。要想对它们进行比较或排序就要调用我们自己实现的MAP函数。在下面的例子中,关键字MAP指明了方法convert()通过把Relational对象影射到REAL型上,来对它们进行排序操作:

CREATETYPErationalASOBJECT(
numINTEGER,
denINTEGER,
MAPMEMBERFUNCTIONCONVERT
RETURNREAL,
...
);

CREATETYPEBODYrationalAS
MAPMEMBERFUNCTIONCONVERT
RETURNREALIS
BEGIN
RETURNnum/den;
ENDCONVERT;
...
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()可以对两个对象进行比较操作:

CREATETYPEcustomerASOBJECT(
IDNUMBER,
NAMEVARCHAR2(20),
addrVARCHAR2(30),
ORDERMEMBERFUNCTIONmatch(ccustomer)
RETURNINTEGER
);

CREATETYPEBODYcustomerAS
ORDERMEMBERFUNCTIONmatch(ccustomer)
RETURNINTEGERIS
BEGIN
IFID<c.IDTHEN
RETURN-1;--anynegativenumberwilldo
ELSIFID>c.IDTHEN
RETURN1;--anypositivenumberwilldo
ELSE
RETURN0;
ENDIF;
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语句来添加、修改或删除属性,并可以为已存在的对象类型添加或删除方法:

CREATETYPEperson_typASOBJECT(
NAMECHAR(20),
ssnCHAR(12),
addressVARCHAR2(100)
);

CREATETYPEperson_ntISTABLEOFperson_typ;

CREATETYPEdept_typASOBJECT(
mgrperson_typ,
empsperson_nt
);

CREATETABLEdeptOFdept_typ;
--AddnewattributestoPerson_typandpropagatethechange
--toPerson_ntanddept_typ
ALTERTYPEperson_typADDATTRIBUTE(pictureBLOB,dobDATE)
CASCADENOTINCLUDINGTABLEDATA;

CREATETYPEmytypeASOBJECT(
attr1NUMBER,
attr2NUMBER
);

ALTERTYPEmytypeADDATTRIBUTE(attr3NUMBER),
DROPATTRIBUTEattr2,
ADDATTRIBUTEattr4NUMBERCASCADE;

在过程编译时,它总是使用当前引用的对象类型版本。在对象类型发生改变时,服务器端引用那个对象类型的过程就变得无效了,在下次过程被调用时它会被自动重新编译。而对于客户端引用被更改过的类型的过程,我们就必须手动编译。

如果从基类删除一个方法,那么也必须修改覆盖被删除方法的子类。我们可以用ALTER TYPE的CASADE选择来判断是否有子类被影响到;如果有子类覆盖了方法,那么语句就会被回滚。为了能成功地从基类删除一个方法,我们可以:

  1. 先从子类删除方法
  2. 从基类删除方法,然后用不带OVERRIDING关键字的ALTER TYPE把它重新添加进去

六、定义对象类型

对象类型可以表现现实世界中的任何实体。例如,一个对象类型能表现学生,银行账户,计算机显示器,有理数或者是像队列,栈,链表这样的数据结构。这一节给出了几个完整的例子,让我们了解如何设计对象类型并编写我们自己的对象类型。

目前我们还不能在PL/SQL块、子程序或包中定义对象类型。但是,我们可以在SQL*Plus中用下面的语法来定义它:

CREATE[ORREPLACE]TYPEtype_name
[AUTHID{CURRENT_USER|DEFINER}]
{{IS|AS}OBJECT|UNDERsupertype_name}
(
attribute_namedatatype[,attribute_namedatatype]...
[{MAP|ORDER}MEMBERfunction_spec,]
[{FINAL|NOTFINAL}MEMBERfunction_spec,]
[{INSTANTIABLE|NOTINSTANTIABLE}MEMBERfunction_spec,]
[{MEMBER|STATIC}{subprogram_spec|call_spec}
[,{MEMBER|STATIC}{subprogram_spec|call_spec}]...]
)[{FINAL|NOTFINAL}][{INSTANTIABLE|NOTINSTANTIABLE}];
[CREATE[ORREPLACE]TYPEBODYtype_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.

CREATETYPEperson_typASOBJECT(
ssnNUMBER,
NAMEVARCHAR2(30),
addressVARCHAR2(100)
)
NOTFINAL;

--Deriveasubtypethathasalltheattributesofthesupertype,
--plussomeadditionalattributes.

CREATETYPEstudent_typUNDERperson_typ(
deptidNUMBER,
majorVARCHAR2(30)
)
NOTFINAL;

--BecauseStudent_typisdeclaredNOTFINAL,youcanderive
--furthersubtypesfromit.

CREATETYPEparttimestudent_typUNDERstudent_typ(
numhoursNUMBER
)
;

--Deriveanothersubtype.Becauseithasthedefaultattribute
--FINAL,youcannotuseEmployee_typasasupertypeandderive
--subtypesfromit.

CREATETYPEemployee_typUNDERperson_typ(
empidNUMBER,
mgrVARCHAR2(30)
)
;

--Defineanobjecttypethatcanbeasupertype.Becausethe
--memberfunctionisFINAL,itcannotbeoverriddeninany
--subtypes.
CREATETYPETASOBJECT(...,MEMBERPROCEDUREPrint(),FINALMEMBER
FUNCTIONfoo(xNUMBER)...)NOTFINAL;
--Weneverwanttocreateanobjectofthissupertype.Byusing
--NOTINSTANTIABLE,weforceallobjectstouseoneofthesubtypes
--instead,withspecificimplementationsforthememberfunctions.
CREATETYPEAddress_typASOBJECT(...)NOTINSTANTIABLENOTFINAL;
--Thesesubtypescanprovidetheirownimplementationsof
--memberfunctions,suchasforvalidatingphonenumbersand
--postalcodes.Becausethereisno"generic"wayofdoingthese
--things,onlyobjectsofthesesubtypescanbeinstantiated.
CREATETYPEUSAddress_typUNDERAddress_typ(...);
CREATETYPEIntlAddress_typUNDERAddress_typ(...);
--ReturnREFsforthosePerson_typobjectsthatareinstancesof
--theStudent_typsubtype,andNULLREFsotherwise.
SELECTTREAT(REF(p)ASREFstudent_typ)
FROMperson_vp;
--ExampleofusingTREATforassignment...
--ReturnREFsforthosePerson_typeobjectsthatareinstancesof
--Employee_typeorStudent_typ,oranyoftheirsubtypes.
SELECTREF(p)
FROMperson_vp
WHEREVALUE(p)ISOF(employee_typ,student_typ);
--Similartoabove,butdonotallowanysubtypesofStudent_typ.
SELECTREF(p)
FROMperson_vp
WHEREVALUE(p)ISOF(ONLYstudent_typ);
--TheresultsofREFandDEREFcanincludeobjectsofPerson_typ
--anditssubtypessuchasEmployee_typandStudent_typ.
SELECTREF(p)
FROMperson_vp;
SELECTDEREF(REF(p))
FROMperson_vp;

2、对象类型实例:栈

栈是一个有序集合。栈有一个栈顶和一个栈底。栈中的每一项都只能在栈顶添加或删除。所以,最后一个被加入栈的项会被最先删除。(可以把栈想象成自助餐厅中的盘子。)压栈和退栈操作能够对栈进行后进先出(LIFO)更新。

栈能应用在很多地方。例如,它们可以用在系统编程中控制中断优先级并对递归进行管理。最简单的栈实现就是使用整数数组,数组的一端代表了栈顶。

PL/SQL提供了VARRAY数据类型,它能让我们声明变长数组。要声明变长数组属性,必须先定义变长数组类型。但是,我们不能再对象说明中定义类型,所以,我们只能单独的定义变长数组类型,并指定它的最大长度,具体实现如下:

CREATETYPEIntArrayASVARRAY(25)OFINTEGER;

现在我们可以编写对象类型说明了:

CREATETYPEstackASOBJECT(
max_sizeINTEGER,
topINTEGER,
POSITIONintarray,
MEMBERPROCEDUREinitialize,
MEMBERFUNCTIONFULL
RETURNBOOLEAN,
MEMBERFUNCTIONempty
RETURNBOOLEAN,
MEMBERPROCEDUREpush(nININTEGER),
MEMBERPROCEDUREpop(nOUTINTEGER)
);

最后,我们可以编写对象类型体:

CREATETYPEBODYstackAS
MEMBERPROCEDUREinitializeIS
BEGIN
top:=0;
/*Callconstructorforvarrayandsetelement1toNULL.*/
POSITION:=intarray(NULL);
max_size:=POSITION.LIMIT;--getvarraysizeconstraint
POSITION.EXTEND(max_size-1,1);--copyelement1into2..25
ENDinitialize;
MEMBERFUNCTIONFULL
RETURNBOOLEANIS
BEGIN
RETURN(top=max_size);--returnTRUEifstackisfull
ENDFULL;
MEMBERFUNCTIONempty
RETURNBOOLEANIS
BEGIN
RETURN(top=0);--returnTRUEifstackisempty
ENDempty;
MEMBERPROCEDUREpush(nININTEGER)IS
BEGIN
IFNOTFULLTHEN
top:=top+1;--pushintegerontostack
POSITION(top):=n;
ELSE--stackisfull
raise_application_error(-20101,'stackoverflow');
ENDIF;
ENDpush;
MEMBERPROCEDUREpop(nOUTINTEGER)IS
BEGIN
IFNOTemptyTHEN
n:=POSITION(top);
top:=top-1;--popintegeroffstack
ELSE
--stackisempty
raise_application_error(-20102,'stackunderflow');
ENDIF;
ENDpop;
END;

在成员过程push和pop中,我们使用内置过程raise_application_error来关联用户定义的错误消息。这样,我们就能把错误报告给客户端程序而避免把未控制异常传给主环境。客户端程序捕获PL/SQL异常后,可以在OTHERS异常控制句柄中用SQLCODE和SQLERRM来确定具体的错误信息。下例中,当异常被抛出时,我们就把对应的错误消息输出:

DECLARE
...
BEGIN
...
EXCEPTION
WHENOTHERSTHEN
dbms_output.put_line(SQLERRM);
END;

另外,程序还可以使用编译指示EXCEPTION_INIT把raise_application_error返回的错误编号影射到命名异常中,如下例所示:

DECLARE
stack_overflowEXCEPTION;
stack_underflowEXCEPTION;
PRAGMAEXCEPTION_INIT(stack_overflow,-20101);
PRAGMAEXCEPTION_INIT(stack_underflow,-20102);
BEGIN
...
EXCEPTION
WHENstack_overflowTHEN
...
END;

3、对象类型实例:售票处

假如有一个连锁电影院,每个影院有三个银幕。每个影院有一个售票处,销售三种不同电影的影票。所有影票的价格都为三美元。定期检查影票的销售情况,然后及时补充影票。

在定义代表销售处的对象类型之前,我们必须考虑到必要的数据和操作。对于一个简单的售票处来说,对象类型需要票价、当前影票存量和已售影票的收据这些属性。它还需要一些方法:购票、盘存、补充存量和收集收据。

对于收据,我们可以使用含有三个元素的数组。元素1、2和3各自记录电影1、2和3。要声明一个变长数组属性,我们就必须先像下面这样定义它的类型:

CREATETYPERealArrayASVARRAY(3)OFREAL;

现在,我们可以编写对象类型说明:

CREATETYPEticket_boothASOBJECT(
priceREAL,
qty_on_handINTEGER,
receiptsrealarray,
MEMBERPROCEDUREinitialize,
MEMBERPROCEDUREpurchase(movieINTEGER,amountREAL,CHANGEOUTREAL),
MEMBERFUNCTIONinventory
RETURNINTEGER,
MEMBERPROCEDUREreplenish(quantityINTEGER),
MEMBERPROCEDURECOLLECT(movieINTEGER,amountOUTREAL)
);

最后,我们可以编写对象类型体:

CREATETYPEBODYticket_boothAS
MEMBERPROCEDUREinitializeIS
BEGIN
price:=3.00;
qty_on_hand:=5000;--provideinitialstockoftickets
--callconstructorforvarrayandsetelements1..3tozero
receipts:=realarray(0,0,0);
ENDinitialize;
MEMBERPROCEDUREpurchase(movieINTEGER,amountREAL,CHANGEOUTREAL)IS
BEGIN
IFqty_on_hand=0THEN
raise_application_error(-20103,'outofstock');
ENDIF;

IFamount>=priceTHEN
qty_on_hand:=qty_on_hand-1;
receipts(movie):=receipts(movie)+price;
CHANGE:=amount-price;
ELSE--amountisnotenough
CHANGE:=amount;--soreturnfullamount
ENDIF;
ENDpurchase;
MEMBERFUNCTIONinventory
RETURNINTEGERIS
BEGIN
RETURNqty_on_hand;
ENDinventory;
MEMBERPROCEDUREreplenish(quantityINTEGER)IS
BEGIN
qty_on_hand:=qty_on_hand+quantity;
ENDreplenish;
MEMBERPROCEDURECOLLECT(movieINTEGER,amountOUTREAL)IS
BEGIN
amount:=receipts(movie);--getreceiptsforagivenmovie
receipts(movie):=0;--resetreceiptstozero
ENDCOLLECT;
END;

4、对象类型实例:银行账户

在定义银行账户对象类型之前,我们必须考虑一下要使用的数据和操作。对于一个简单的银行账户来说,对象类型需要一个账号、余额和状态这三个属性。所需的操作有:打开帐户,验证账号,关闭账户,存款,取款和余额结算。

首先,我们要像下面这样编写对象类型说明:

CREATETYPEbank_accountASOBJECT(
acct_numberINTEGER(5),
balanceREAL,
statusVARCHAR2(10),
MEMBERPROCEDUREOPEN(amountINREAL),
MEMBERPROCEDUREverify_acct(numININTEGER),
MEMBERPROCEDURECLOSE(numININTEGER,amountOUTREAL),
MEMBERPROCEDUREdeposit(numININTEGER,amountINREAL),
MEMBERPROCEDUREwithdraw(numININTEGER,amountINREAL),
MEMBERFUNCTIONcurr_bal(numININTEGER)
RETURNREAL
);

然后编写对象体:

CREATETYPEBODYbank_accountAS
MEMBERPROCEDUREOPEN(amountINREAL)IS
--openaccountwithinitialdeposit
BEGIN
IFNOTamount>0THEN
raise_application_error(-20104,'badamount');
ENDIF;

SELECTacct_sequence.NEXTVAL
INTOacct_number
FROMDUAL;

status:='open';
balance:=amount;
ENDOPEN;
MEMBERPROCEDUREverify_acct(numININTEGER)IS
--checkforwrongaccountnumberorclosedaccount
BEGIN
IF(num<>acct_number)THEN
raise_application_error(-20105,'wrongnumber');
ELSIF(status='closed')THEN
raise_application_error(-20106,'accountclosed');
ENDIF;
ENDverify_acct;
MEMBERPROCEDURECLOSE(numININTEGER,amountOUTREAL)IS
--closeaccountandreturnbalance
BEGIN
verify_acct(num);
status:='closed';
amount:=balance;
ENDCLOSE;
MEMBERPROCEDUREdeposit(numININTEGER,amountINREAL)IS
BEGIN
verify_acct(num);

IFNOTamount>0THEN
raise_application_error(-20104,'badamount');
ENDIF;

balance:=balance+amount;
ENDdeposit;
MEMBERPROCEDUREwithdraw(numININTEGER,amountINREAL)IS
--ifaccounthasenoughfunds,withdraw
--givenamount;else,raiseanexception
BEGIN
verify_acct(num);

IFamount<=balanceTHEN
balance:=balance-amount;
ELSE
raise_application_error(-20107,'insufficientfunds');
ENDIF;
ENDwithdraw;
MEMBERFUNCTIONcurr_bal(numININTEGER)
RETURNREALIS
BEGIN
verify_acct(num);
RETURNbalance;
ENDcurr_bal;
END;

5、对象类型实例:实数

有理数能够表现成两个整数相除的形式,一个分子和一个分母。同大多数语言一样,PL/SQL并没有实数类型或是用于实数操作的预定义操作符。现在让我们就用对象类型来弥补这个缺失。首先,编写下面的对象说明:

CREATETYPErationalASOBJECT(
numINTEGER,
denINTEGER,
MAPMEMBERFUNCTIONCONVERT
RETURNREAL,
MEMBERPROCEDUREnormalize,
MEMBERFUNCTIONreciprocal
RETURNrational,
MEMBERFUNCTIONplus(xrational)
RETURNrational,
MEMBERFUNCTIONLESS(xrational)
RETURNrational,
MEMBERFUNCTIONtimes(xrational)
RETURNrational,
MEMBERFUNCTIONdivby(xrational)
RETURNrational,
PRAGMARESTRICT_REFERENCES(DEFAULT,RNDS,WNDS,RNPS,WNPS)
);

PL/SQL不允许操作符重载。所以我们必须定义方法plus(),less()(minus是保留关键字),times()和divby()来替代操作符+、-、*和/。

下一步,创建下面的独立存储函数,它们会被方法normalize()调用:

CREATEFUNCTIONgcd(xINTEGER,yINTEGER)
RETURNINTEGERAS
--findgreatestcommondivisorofxandy
ansINTEGER;
BEGIN
IF(y<=x)AND(xMODy=0)THEN
ans:=y;
ELSIFx<yTHEN
ans:=gcd(y,x);--recursivecall
ELSE
ans:=gcd(y,xMODy);--recursivecall
ENDIF;

RETURNans;
END;

下面是对象类型体的内容:

CREATETYPEBODYrationalAS
MAPMEMBERFUNCTIONCONVERT
RETURNREALIS
--convertrationalnumbertorealnumber
BEGIN
RETURNnum/den;
ENDCONVERT;
MEMBERPROCEDUREnormalizeIS
--reducefractionnum/dentolowestterms
gINTEGER;
BEGIN
g:=gcd(num,den);
num:=num/g;
den:=den/g;
ENDnormalize;
MEMBERFUNCTIONreciprocal
RETURNrationalIS
--returnreciprocalofnum/den
BEGIN
RETURNrational(den,num);--callconstructor
ENDreciprocal;
MEMBERFUNCTIONplus(xrational)
RETURNrationalIS
--returnsumofSELF+x
rrational;
BEGIN
r:=rational(num*x.den+x.num*den,den*x.den);
r.normalize;
RETURNr;
ENDplus;
MEMBERFUNCTIONLESS(xrational)
RETURNrationalIS
--returndifferenceofSELF-x
rrational;
BEGIN
r:=rational(num*x.den-x.num*den,den*x.den);
r.normalize;
RETURNr;
ENDLESS;
MEMBERFUNCTIONtimes(xrational)
RETURNrationalIS
--returnproductofSELF*x
rrational;
BEGIN
r:=rational(num*x.num,den*x.den);
r.normalize;
RETURNr;
ENDtimes;
MEMBERFUNCTIONdivby(xrational)
RETURNrationalIS
--returnquotientofSELF/x
rrational;
BEGIN
r:=rational(num*x.den,den*x.num);
r.normalize;
RETURNr;
ENDdivby;
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
...
PROCEDUREopen_acct(new_acctINOUTAccount)IS...

下面,我们把Accout作为函数的返回类型来使用:

DECLARE
...
FUNCTIONget_acct(acct_idININTEGER)RETURNAccountIS...

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
IFrISNULLTHEN...--conditionyieldsTRUE

一个好的编程习惯就是在声明的时候就为对象初始化,例如:

DECLARE
rRational:=Rational(2,3);--rbecomes2/3

3、PL/SQL如何对待未初始化对象

在表达式中,未初始化的对象属性值为NULL。如果为一个未初始化的对象属性赋值,就会引起预定义异常ACCESS_INTO_NULL。当对未初始化的对象或是它的属性使用IS NULL做比较时,结果总为TRUE。

下面的例子就演示了空对象和含有空属性的对象之间的差异:

DECLARE
rrational;--risatomicallynull
BEGIN
IFrISNULLTHEN...--yieldsTRUE
IFr.numISNULLTHEN...--yieldsTRUE
r:=rational(NULL,NULL);--initializesr
r.num:=4;--succeedsbecauserisnolongeratomicallynull
--eventhoughallitsattributesarenull
r:=NULL;--rbecomesatomicallynullagain
r.num:=4;--raisesACCESS_INTO_NULL
EXCEPTION
WHENACCESS_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;

下面再看一个稍微复杂一点的对象嵌套例子:

CREATETYPEaddressASOBJECT(
streetVARCHAR2(30),
cityVARCHAR2(20),
stateCHAR(2),
zip_codeVARCHAR2(5)
);

CREATETYPEstudentASOBJECT(
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

九、定义构造函数

默认情况下,我们不需要为对象类型定义构造函数,因为系统会提供一个接受与每个属性相对应的参数的构造函数。

我们也许想定义自己的构造函数:

  1. 为某些属性提供默认值,这样就能确保属性值的正确性而不必依赖于调用者所提供的每一个属性值。
  2. 避免许多特殊用途的过程只初始化对象的不同部分。
  3. 当新的属性加到对象类型中时,避免更改调用构造函数的应用程序中的代码。构造函数也许需要一些新的代码,例如把属性设置为空,但我们还需要保持方法签名不变,这样可以使已存在的构造函数调用继续工作。
CREATEORREPLACETYPErectangleASOBJECT(
--Thetypehas3attributes.
LENGTHNUMBER,
widthNUMBER,
areaNUMBER,
--Defineaconstructorthathasonly2parameters.
CONSTRUCTORFUNCTIONrectangle(LENGTHNUMBER,widthNUMBER)
RETURNSELFASRESULT
);
/

CREATEORREPLACETYPEBODYrectangleAS
CONSTRUCTORFUNCTIONrectangle(LENGTHNUMBER,widthNUMBER)
RETURNSELFASRESULTAS
BEGIN
SELF.LENGTH:=LENGTH;
SELF.width:=width;
--Wecomputethearearatherthanacceptingitasaparameter.
SELF.area:=LENGTH*width;
RETURN;
END;
END;
/

DECLARE
r1rectangle;
r2rectangle;
BEGIN
--Wecanstillcallthedefaultconstructor,withall3parameters.
r1:=NEWrectangle(10,20,200);
--Butitismorerobusttocallourconstructor,whichcomputes
--theAREAattribute.ThisguaranteesthattheinitialvalueisOK.
r2:=NEWrectangle(10,20);
END;
/

十、调用构造函数

只要是能够调用普通函数的地方,我们就可以调用构造函数。跟所有的函数一样,构造函数也可以作为表达式的一部分而被调用,如下例所示:

DECLARE
r1rational:=rational(2,3);

FUNCTIONaverage(xrational,yrational)
RETURNrationalIS
BEGIN
...
END;
BEGIN
r1:=average(rational(3,4),rational(7,11));

IF(rational(5,8)>r1)THEN
...
ENDIF;
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修饰符共享对象

在真实世界中的大多数对象都要比有理数类型庞大而且复杂。如果对象比较大的话,把对象副本从一个子程序传递到另一个子程序时效率就可能会很低。这时如果使用对象共享就很有意义了,我们可以使用一个指向对象的引用来引用所需要的对象。

共享有两个重要的好处。首先,避免了不必要的数据重复。其次,在共享的对象内容更新时,任何引用所指向的内容也会被立即更新。如下面的例子:

CREATETYPEhomeASOBJECT(
addressVARCHAR2(35),
ownerVARCHAR2(25),
ageINTEGER,
styleVARCHAR(15),
floor_planBLOB,
priceREAL(9,2),
...
);
/

CREATETABLEhomesOFhome;

修改一下Person,我们就能建立家庭的模型,几个人共享一个家。我们可以使用修饰符REF来声明引用:

CREATETYPEpersonASOBJECT(
first_nameVARCHAR2(10),
last_nameVARCHAR2(15),
birthdayDATE,
home_addressREFhome,--canbesharedbyfamily
phone_numberVARCHAR2(15),
ss_numberINTEGER,
motherREFperson,--familymembersrefertoeachother
fatherREFperson,
...
);

我们可以把变量、参数、字段或属性声明为引用。而且,我们还可以在SQL数据操作语句中把引用当作输入或输出变量使用。但是,我们不可以通过引用调用对象的内容。比如表达式x.attribute,其中x是一个引用,PL/SQL无法找到存放对象的数据表。例如,下面的赋值语句就是不允许的:

DECLARE
p_refREFperson;
phone_noVARCHAR2(15);
BEGIN
phone_no:=p_ref.phone_number;--notallowed
END;

解决办法就是用函数DEREF或调用包UTL_REF来访问对象。

1、向前类型定义

我们只能引用已经存在的模式对象。下例中,第一个CREATE TYPE语句是不允许的,因为它引用的Department并不存在:

CREATETYPEemployeeASOBJECT(
NAMEVARCHAR2(20),
deptREFdepartment,--notallowed
...
);

CREATETYPEdepartmentASOBJECT(
"number"INTEGER,
manageremployee,
...
);

把上面两个CREATE TYPE语句调换位置也不会起作用,因为这两个对象类型是互相依赖的。对象类型Employee有着一个Department类型的属性,而Department又同样有着一个Employee类型的属性。为了解决这个问题,我们应该使用特殊的CREATE TYPE语句,我称它为向前类型定义,这样我们就可以互相引用两个独立的对象类型。现在使用下面的语句:

要调试上面的例子,我们只要把下面的语句提前即可:

CREATETYPEDepartment;--forwardtypedefinition
--atthispoint,Departmentisanincompleteobjecttype

向前类型定义创建的对象类型被称为不完全对象类型(incomplete object type),因为它没有属性和方法。一个不纯的不完全对象类型有属性,但编译时会发生错误,因为它引用了一个未确定的类型。例如,下面的CREATE TYPE语句就会因对象类型Address未确定而出错:

CREATETYPECustomerASOBJECT(
idNUMBER,
nameVARCHAR2(20),
addrAddress,--notyetdefined
phoneVARCHAR2(15)
);

如果使用了向前声明,我们就可以推迟对象类型Address的定义,并且,不完全类型Customer也可以被其他应用程序的开发者引用。

十三、操作对象

我们可以在CREATE TABLE语句中把某个字段指定为对象类型。一旦表被建立,我们就可以用SQL语句把对象插入表中,选取它的属性,调用它的方法更新它的状态。

注意:访问远程的或分布式对象都是不允许的。

在下面SQL*Plus脚本中,INSERT语句调用对象类型Rational的构造函数,然后插入resulting对象。SELECT语句检索属性num的值。UPDATE语句调用成员方法reciprocal(),在交换属性num和den之后,返回Retional的值。要注意的是,在引用属性或方法时,表别名是必须的。

CREATETABLEnumbers(rnRational,...)
/
INSERTINTOnumbers(rn)VALUES(Rational(3,62))--inserts3/62
/
SELECTn.rn.numINTOmy_numFROMnumbersn...--returns3
/
UPDATEnumbersnSETn.rn=n.rn.reciprocal()...--yields62/3

用这种方法初始化对象时,对象在数据库表之外是没有标识的。但是,对象类型是独立于表而存在的,可以用其他方式创建对象。

下例中,我们创建一个存放对象类型Retional的表。这样包含对象类型的表称为对象表。一行中的每一列都与对象的属性对应。行与行间的列值是不同的。

CREATETABLErational_numsOFRational;

对象表中每行都有一个对象标识,能够唯一辨识一个存放在数据库中的对象。

1、查询对象

假定我们要在SQL*Plus中运行下面的脚本,创建一个对象类型Person和一个对象表persons,并且我们为该表填充一些数据:

CREATETYPEpersonASOBJECT(
first_nameVARCHAR2(15),
last_nameVARCHAR2(15),
birthdayDATE,
home_addressaddress,
phone_numberVARCHAR2(15)
)
/

CREATETABLEpersonsOFperson
/

下面子查询能产生一个只包含Person对象属性的结果集:

BEGIN
INSERTINTOemployees--anotherobjecttableoftypePerson
SELECT*
FROMpersonsp
WHEREp.last_nameLIKE'%Smith';

要返回对象结果集,我们就必须使用VALUE函数。

  • 使用VALUE函数

跟我们所期望的一样,函数VALUE能返回对象值。VALUE会把一个相关的变量作为它的参数。(在这里,相关变量就是行变量或与对象表中的一行相关联的表别名)。例如,要返回Person对象的结果集,要向下面这样使用VALUE:

BEGIN
INSERTINTOemployees
SELECTVALUE(p)
FROMpersonsp
WHEREp.last_nameLIKE'%Smith';

在下面的例子中,我们可以使用VALUE来返回一个特定的Person对象:

DECLARE
p1person;
p2person;
...
BEGIN
SELECTVALUE(p)
INTOp1
FROMpersonsp
WHEREp.last_name='Kroll';

p2:=p1;
...
END;

p1是一个本地的Person对象,它是名为"Kroll"的存储对象的一个副本,而p2是另一个本地Person对象,它是p1的副本。如下例所示,我们可以使用这些变量来访问和更新它们所引用的对象:

BEGIN
p1.last_name:=p1.last_name||'Jr';
END;

现在,本地的Person对象p1的名字为"Kroll Jr"。

  • 使用REF函数

我们可以用函数REF来检索引用,同VALUE一样,它也把一个相关的变量作为它的参数。下例中,我们检索一个或多个指向Person对象的引用,然后把引用插入表person_refs中:

BEGIN
INSERTINTOperson_refs
SELECTREF(p)
FROMpersonsp
WHEREp.last_nameLIKE'%Smith';
END;

下面的例子我们同时检索一个引用和一个属性:

DECLARE
p_refREFperson;
taxpayer_idVARCHAR2(9);
BEGIN
SELECTREF(p),p.ss_number
INTOp_ref,taxpayer_id
FROMpersonsp
WHEREp.last_name='Parker';--mustreturnonerow
...
END;

在最后一个例子中,我们更新一个Person对象的属性:

DECLARE
p_refREFperson;
my_last_nameVARCHAR2(15);
BEGIN
SELECTREF(p)
INTOp_ref
FROMpersonsp
WHEREp.last_name=my_last_name;

UPDATEpersonsp
SETp=person('Jill','Anders','11-NOV-67',...)
WHEREREF(p)=p_ref;
END;
  • 测试dangling引用

如果一个引用所指向的对象被删除了,那么它就会指向一个不存在的对象,我们称这样的引用为dangling引用。要测试这种情况,我们应该使用SQL的IS DANGLING语句。假设关系表department的manager字段引用了对象表中的Employee对象。我们就可以使用下面的UPDATE语句来把"dangling"引用转为空值:

UPDATEdepartment
SETmanager=NULL
WHEREmanagerISDANGLING;
  • 使用DEREF函数

我们不可以在PL/SQL过程语句中。(DEREF是dereference的缩写。当我们反引用一个指针时,我们就能获取它所指向的值。)DEREF把对象的引用作为它的参数值,然后返回这个引用所指向的对象。如果引用是"dangling"的,DEREF就会返回一个空对象。

在下面的例子中,我们可以反引用一个指向Person对象的引用。要注意的是,我们是从dummy表dual中取得引用值的。我们不需要指定对象表和检索条件,因为每个存放于对象表的对象都有唯一的互斥的对象标识,它是每一个指向对象的引用的一部分。

DECLARE
p1person;
p_refREFperson;
NAMEVARCHAR2(15);
BEGIN
...
/*Assumethatp_refholdsavalidreference
toanobjectstoredinanobjecttable.*/

SELECTDEREF(p_ref)
INTOp1
FROMDUAL;

NAME:=p1.last_name;
END;

我们可以在后续的SQL语句中用DEREF进行反引用操作,如下例所示:

CREATETYPEpersonrefASOBJECT(
p_refREFperson
)
/

DECLARE
NAMEVARCHAR2(15);
pr_refREFpersonref;
prpersonref;
pperson;
BEGIN
...
/*Assumepr_refholdsavalidreference.*/
SELECTDEREF(pr_ref)
INTOpr
FROMDUAL;

SELECTDEREF(pr.p_ref)
INTOp
FROMDUAL;
...
END;
/

下面是一个我们不能在过程语句中使用DEREF函数的例子:

BEGIN
...
p1:=DEREF(p_ref);--notallowed

在SQL语句中,我们可以使用点标志通过一个对象字段来引用它的属性,也可以通过一个对象字段的属性引用另一个属性。如果使用了表别名,那么就还能通过引用字段来访问属性。例如,下面的语法就是有效的:

table_alias.object_column.ref_attribute
table_alias.object_column.ref_attribute.attribute
table_alias.ref_column.attribute

假设我们在SQL*Plus中运行下面的脚本来创建对象类型Address和Person,对象表persons:

CREATETYPEaddressASOBJECT(
streetVARCHAR2(35),
cityVARCHAR2(15),
stateCHAR(2),
zip_codeINTEGER
)
/

CREATETYPEpersonASOBJECT(
first_nameVARCHAR2(15),
last_nameVARCHAR2(15),
birthdayDATE,
home_addressREFaddress,--sharedwithotherPersonobjects
phone_numberVARCHAR2(15)
)
/

CREATETABLEpersonsOFperson
/

引用属性home_address对应对象表persons中的一个引用,该引用指向存放在其他表中的Address对象的字段。填充数据表之后,我们就可以像下面这样反引用它的引用来得到特定的address:

DECLARE
addr1Address;
addr2Address;
...
BEGIN
SELECTDEREF(home_address)
INTOaddr1
FROMpersonsp
WHEREp.last_name='Derringer';
END;

在下面的例子中,我们可以通过引用字段home_address来找到属性street。这种情况下,我们必须要使用表别名。

DECLARE
my_streetVARCHAR2(25);
...
BEGIN
SELECTp.home_address.street
INTOmy_street
FROMpersonsp
WHEREp.last_name='Lucas';
END;

2、插入对象

我们可以使用insert语句为对象表添加一条记录。下例中,我们向对象表persons插入一个Person对象:

BEGIN
INSERTINTOpersons
VALUES('Jenifer','Lapidus',...);
END;

另外,我们还可以使用对象类型Person的构造函数来插入记录:

BEGIN
INSERTINTOpersons
VALUES(person('Albert','Brooker',...));
END;

下例中,我们可以使用RETURNING子句把对象Person的引用保存在本地变量中。注意一下这个子句是如何模拟SELECT语句的。我们也可以在UPDATE和DELETE语句中使用RETURNING子句。

DECLARE
p1_refREFperson;
p2_refREFperson;
BEGIN
INSERTINTOpersonsp
VALUES(Person('Paul','Chang',...))
RETURNINGREF(p)
INTOp1_ref;

INSERTINTOpersonsp
VALUES(Person('Ana','Thorne',...))
RETURNINGREF(p)
INTOp2_ref;
END;

要往对象表中插入对象,我们还可以利用返回同样对象类型的子查询来进行操作。示例如下:

BEGIN
INSERTINTOpersons2
SELECTVALUE(p)
FROMpersonsp
WHEREp.last_nameLIKE'%Jones';
END;

拷贝到对象表person2的行被赋予新的对象标识。对象标识是不会从对象表persons中拷贝出来的。

下面的脚本创建一个名为department的关系表,其中有一个类型为Person的字段,然后向表中插入一条记录。注意如何使用构造函数Person()为字段manager提供值。

CREATETABLEdepartment(
dept_nameVARCHAR2(20),
managerperson,
LOCATIONVARCHAR2(20))
/
INSERTINTOdepartment
VALUES('Payroll',Person('Alan','Tsai',...),'LosAngeles')
/

存放到字段manager中的新Person对象是不能被引用的,因为它是被存放在字段中(不是行中)的,也就没有对象标识。

3、更新对象

如果要修改对象表中的对象属性,我们可以像下面这样使用UPDATE语句:

BEGIN
UPDATEpersonsp
SETp.home_address='341OakdeneAve'
WHEREp.last_name='Brody';

UPDATEpersonsp
SETp=person('Beth','Steinberg',...)
WHEREp.last_name='steinway';
END;

4、删除对象

我们可以用DELETE语句从对象表中删除对象。要有选择的删除对象,我们就得在WHERE子句中进行指定:

BEGIN
DELETEFROMpersonsp
WHEREp.home_address='108PalmDr';
END;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值