七、声明并初始化对象
只要对象类型在模式中定义了,我们就可以在任何PL/SQL块、子程序或包中引用它来声明对象。例如,我们可以使用对象类型作为属性、字段、变量、绑定变量、记录的域、表元的素、形式参数或函数返回值的数据类型。在运行时,对象类型的实例会被创建,也就是对象实例被初始化。
1、定义对象
我们可以在使用内置类型(如CHAR或NUMBER)的地方使用对象类型。在下面的块中,我们声明了Rational类型的对象r。然后调用构造函数初始化对象,把值6和8分别赋给属性num和den。
DECLARE
r rational;
BEGIN
r := rational(6, 8);
DBMS_OUTPUT.put_line(r.num); -- prints 6
END;
我们还可以把对象作为函数和过程的形式参数。那样就能把对象从一个子程序传递到另一个子程序了。在下面的例子中,我们使用对象类型Account作为形式参数:
DECLARE
...
PROCEDURE open_acct (new_acct IN OUT Account) IS ...
下面,我们把Accout作为函数的返回类型来使用:
DECLARE
...
FUNCTION get_acct (acct_id IN INTEGER) RETURN Account IS ...
2、初始化对象
如果不调用构造函数初始化对象,那它会被自动赋上空值。也就是说对象本身为空,不单单指它的属性。如下例:
DECLARE
r rational; -- r becomes atomically null
BEGIN
r := rational(2, 3); -- r becomes 2/3
END;
一个空对象总不能等于另一个对象。实际上,拿一个空对象与另一个对象比较结果总是NULL.同样,如果把一个空对象或NULL赋给另一个对象,那么被赋值的对象也为空,示例如下:
DECLARE
r rational;
BEGIN
r := rational(1, 2); -- r becomes 1/2
r := NULL; -- r becomes atomically null
IF r IS NULL THEN ... -- condition yields TRUE
一个好的编程习惯就是在声明的时候就为对象初始化,例如:
DECLARE
r Rational := Rational(2,3); -- r becomes 2/3
3、PL/SQL如何对待未初始化对象
在表达式中,未初始化的对象属性值为NULL。如果为一个未初始化的对象属性赋值,就会引起预定义异常ACCESS_INTO_NULL。当对未初始化的对象或是它的属性使用IS NULL做比较时,结果总为TRUE。
下面的例子就演示了空对象和含有空属性的对象之间的差异:
DECLARE
r rational; -- r is atomically null
BEGIN
IF r IS NULL THEN ... -- yields TRUE
IF r.num IS NULL THEN ... -- yields TRUE
r := rational(NULL, NULL); -- initializes r
r.num := 4; -- succeeds because r is no longer atomically null
-- even though all its attributes are null
r := NULL; -- r becomes atomically null again
r.num := 4; -- raises ACCESS_INTO_NULL
EXCEPTION
WHEN ACCESS_INTO_NULL THEN
...;
END;
调用一个未初始化对象的方法会引起预定义异常NULL_SELF_DISPATCH。如果把未初始化对象的属性作为IN模式参数进行传递时,就跟传递一个NULL参数一样;如果把它作为OUT或IN OUT模式参数传递,并且尝试为其赋值,就会引起异常。
八、访问属性
我们只能按名称来引用属性,不可以使用它的位置来进行引用。在下面的例子中,我们先把属性den赋给变量denominator,然后把变量numerator的值赋给属性num。
DECLARE
r rational := rational(NULL, NULL);
numerator INTEGER;
denominator INTEGER;
BEGIN
...
denominator := r.den;
r.num := numerator;
END;
下面再看一个稍微复杂一点的对象嵌套例子:
CREATE TYPE address AS OBJECT(
street VARCHAR2(30),
city VARCHAR2(20),
state CHAR(2),
zip_code VARCHAR2(5)
);
CREATE TYPE student AS OBJECT(
NAME VARCHAR2(20),
home_address address,
phone_number VARCHAR2(10),
status varcahr2(10),
advisor_name VARCHAR2(20),
...
);
这里要注意的是,zip_code是对象类型Address的一个属性,而Address又是对象Student的属性home_address的数据类型。如果s是Student的对象的话,我们就可以像下面这样访问它的zip_code属性:
s.home_address.zip_code
九、定义构造函数
默认情况下,我们不需要为对象类型定义构造函数,因为系统会提供一个接受与每个属性相对应的参数的构造函数。
我们也许想定义自己的构造函数:
- 为某些属性提供默认值,这样就能确保属性值的正确性而不必依赖于调用者所提供的每一个属性值。
- 避免许多特殊用途的过程只初始化对象的不同部分。
- 当新的属性加到对象类型中时,避免更改调用构造函数的应用程序中的代码。构造函数也许需要一些新的代码,例如把属性设置为空,但我们还需要保持方法签名不变,这样可以使已存在的构造函数调用继续工作。
CREATE OR REPLACE TYPE rectangle AS OBJECT(
-- The type has 3 attributes.
LENGTH NUMBER,
width NUMBER,
area NUMBER,
-- Define a constructor that has only 2 parameters.
CONSTRUCTOR FUNCTION rectangle(LENGTH NUMBER, width NUMBER)
RETURN SELF AS RESULT
);
/
CREATE OR REPLACE TYPE BODY rectangle AS
CONSTRUCTOR FUNCTION rectangle(LENGTH NUMBER, width NUMBER)
RETURN SELF AS RESULT AS
BEGIN
SELF.LENGTH := LENGTH;
SELF.width := width;
-- We compute the area rather than accepting it as a parameter.
SELF.area := LENGTH * width;
RETURN;
END;
END;
/
DECLARE
r1 rectangle;
r2 rectangle;
BEGIN
-- We can still call the default constructor, with all 3 parameters.
r1 := NEW rectangle(10, 20, 200);
-- But it is more robust to call our constructor, which computes
-- the AREA attribute. This guarantees that the initial value is OK.
r2 := NEW rectangle(10, 20);
END;
/
十、调用构造函数
只要是能够调用普通函数的地方,我们就可以调用构造函数。跟所有的函数一样,构造函数也可以作为表达式的一部分而被调用,如下例所示:
DECLARE
r1 rational := rational(2, 3);
FUNCTION average(x rational, y rational)
RETURN rational IS
BEGIN
...
END;
BEGIN
r1 := average(rational(3, 4), rational(7, 11));
IF (rational(5, 8) > r1) THEN
...
END IF;
END;
当我们为构造函数传递参数的时候,调用会把对象中需要初始化的属性赋上初始值。如果是调用默认的构造函数,我们就需要为每个属性指定一个初始值;跟常量和变量不同,属性是不可以有默认值的。下例中,第n个参数为第n个属性赋值:
DECLARE
r rational;
BEGIN
r := rational(5, 6); -- assign 5 to num, 6 to den
-- now r is 5/6
十一、调用方法
跟打包子程序一样,方法也是使用点标志来调用的。示例如下:
DECLARE
r rational;
BEGIN
r := rational(6, 8);
r.normalize;
DBMS_OUTPUT.put_line(r.num); -- prints 3
END;
如下例所示,我们可以连续调用方法。执行顺序从左到右。首先,成员函数reciprocal()会被调用,然后成员过程normalize()被调用。
DECLARE
r rational := rational(6, 8);
BEGIN
r.reciprocal().normalize;
DBMS_OUTPUT.put_line(r.num); -- prints 4
END;
在SQL语句中,调用无参数的方法需要使用一个空的参数列表。在过程化语句中,空的参数列表是可选的,除非我们使用链式调用,这时除了最后一个调用之外其他的都需要空的参数列表。
我们不能把过程作为连续调用的一部分,因为过程是作为语句使用而不是表达式。所以,像下面这样的语句是不允许的:
r.normalize().reciprocal; -- not allowed
同样,如果连续调用两个函数,第一个函数必须返回一个能传入第二个函数的对象。对于静态方法,我们使用下面的语法:
type_name.method_name
这种调用是不需要对象实例的。从子类实例中调用方法时,实际执行的方法是由类的继承关系决定的。如果子类覆盖了基类的方法,子类的方法就会被调用;否则的话,基类的方法会被调用。这种情况称为动态方法分派。
十二、通过REF修饰符共享对象
在真实世界中的大多数对象都要比有理数类型庞大而且复杂。如果对象比较大的话,把对象副本从一个子程序传递到另一个子程序时效率就可能会很低。这时如果使用对象共享就很有意义了,我们可以使用一个指向对象的引用来引用所需要的对象。
共享有两个重要的好处。首先,避免了不必要的数据重复。其次,在共享的对象内容更新时,任何引用所指向的内容也会被立即更新。如下面的例子:
CREATE TYPE home AS OBJECT(
address VARCHAR2(35),
owner VARCHAR2(25),
age INTEGER,
style VARCHAR(15),
floor_plan BLOB,
price REAL(9, 2),
...
);
/
CREATE TABLE homes OF home;
修改一下Person,我们就能建立家庭的模型,几个人共享一个家。我们可以使用修饰符REF来声明引用:
CREATE TYPE person AS OBJECT(
first_name VARCHAR2(10),
last_name VARCHAR2(15),
birthday DATE,
home_address REF home, -- can be shared by family
phone_number VARCHAR2(15),
ss_number INTEGER,
mother REF person, -- family members refer to each other
father REF person,
...
);
我们可以把变量、参数、字段或属性声明为引用。而且,我们还可以在SQL数据操作语句中把引用当作输入或输出变量使用。但是,我们不可以通过引用调用对象的内容。比如表达式x.attribute,其中x是一个引用,PL/SQL无法找到存放对象的数据表。例如,下面的赋值语句就是不允许的:
DECLARE
p_ref REF person;
phone_no VARCHAR2(15);
BEGIN
phone_no := p_ref.phone_number; -- not allowed
END;
解决办法就是用函数DEREF或调用包UTL_REF来访问对象。
1、向前类型定义
我们只能引用已经存在的模式对象。下例中,第一个CREATE TYPE语句是不允许的,因为它引用的Department并不存在:
CREATE TYPE employee AS OBJECT(
NAME VARCHAR2(20),
dept REF department, -- not allowed
...
);
CREATE TYPE department AS OBJECT(
"number" INTEGER,
manager employee,
...
);
把上面两个CREATE TYPE语句调换位置也不会起作用,因为这两个对象类型是互相依赖的。对象类型Employee有着一个Department类型的属性,而 Department又同样有着一个Employee类型的属性。为了解决这个问题,我们应该使用特殊的CREATE TYPE语句,我称它为向前类型定义,这样我们就可以互相引用两个独立的对象类型。现在使用下面的语句:
要调试上面的例子,我们只要把下面的语句提前即可:
CREATE TYPE Department; -- forward type definition
-- at this point, Department is an incomplete object type
向前类型定义创建的对象类型被称为不完全对象类型(incomplete object type),因为它没有属性和方法。一个不纯的不完全对象类型有属性,但编译时会发生错误,因为它引用了一个未确定的类型。例如,下面的CREATE TYPE语句就会因对象类型Address未确定而出错:
CREATE TYPE Customer AS OBJECT (
id NUMBER,
name VARCHAR2(20),
addr Address, -- not yet defined
phone VARCHAR2(15)
);
如果使用了向前声明,我们就可以推迟对象类型Address的定义,并且,不完全类型Customer也可以被其他应用程序的开发者引用。
十三、操作对象
我们可以在CREATE TABLE语句中把某个字段指定为对象类型。一旦表被建立,我们就可以用SQL语句把对象插入表中,选取它的属性,调用它的方法更新它的状态。
注意:访问远程的或分布式对象都是不允许的。
在下面SQL*Plus脚本中,INSERT语句调用对象类型Rational的构造函数,然后插入resulting对象。SELECT语句检索属性num的值。UPDATE语句调用成员方法reciprocal(),在交换属性num和den之后,返回Retional的值。要注意的是,在引用属性或方法时,表别名是必须的。
CREATE TABLE numbers (rn Rational, ...)
/
INSERT INTO numbers (rn) VALUES (Rational(3, 62)) -- inserts 3/62
/
SELECT n.rn.num INTO my_num FROM numbers n ... -- returns 3
/
UPDATE numbers n SET n.rn = n.rn.reciprocal() ... -- yields 62/3
用这种方法初始化对象时,对象在数据库表之外是没有标识的。但是,对象类型是独立于表而存在的,可以用其他方式创建对象。
下例中,我们创建一个存放对象类型Retional的表。这样包含对象类型的表称为对象表。一行中的每一列都与对象的属性对应。行与行间的列值是不同的。
CREATE TABLE rational_nums OF Rational;
对象表中每行都有一个对象标识,能够唯一辨识一个存放在数据库中的对象。
1、查询对象
假定我们要在SQL*Plus中运行下面的脚本,创建一个对象类型Person和一个对象表persons,并且我们为该表填充一些数据:
CREATE TYPE person AS OBJECT(
first_name VARCHAR2(15),
last_name VARCHAR2(15),
birthday DATE,
home_address address,
phone_number VARCHAR2(15)
)
/
CREATE TABLE persons OF person
/
下面子查询能产生一个只包含Person对象属性的结果集:
BEGIN
INSERT INTO employees -- another object table of type Person
SELECT *
FROM persons p
WHERE p.last_name LIKE '%Smith';
要返回对象结果集,我们就必须使用VALUE函数。
- 使用VALUE函数
跟我们所期望的一样,函数VALUE能返回对象值。VALUE会把一个相关的变量作为它的参数。(在这里,相关变量就是行变量或与对象表中的一行相关联的表别名)。例如,要返回Person对象的结果集,要向下面这样使用VALUE:
BEGIN
INSERT INTO employees
SELECT VALUE(p)
FROM persons p
WHERE p.last_name LIKE '%Smith';
在下面的例子中,我们可以使用VALUE来返回一个特定的Person对象:
DECLARE
p1 person;
p2 person;
...
BEGIN
SELECT VALUE(p)
INTO p1
FROM persons p
WHERE p.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
INSERT INTO person_refs
SELECT REF(p)
FROM persons p
WHERE p.last_name LIKE '%Smith';
END;
下面的例子我们同时检索一个引用和一个属性:
DECLARE
p_ref REF person;
taxpayer_id VARCHAR2(9);
BEGIN
SELECT REF(p), p.ss_number
INTO p_ref, taxpayer_id
FROM persons p
WHERE p.last_name = 'Parker'; -- must return one row
...
END;
在最后一个例子中,我们更新一个Person对象的属性:
DECLARE
p_ref REF person;
my_last_name VARCHAR2(15);
BEGIN
SELECT REF(p)
INTO p_ref
FROM persons p
WHERE p.last_name = my_last_name;
UPDATE persons p
SET p = person('Jill', 'Anders', '11-NOV-67', ...)
WHERE REF(p) = p_ref;
END;
- 测试dangling引用
如果一个引用所指向的对象被删除了,那么它就会指向一个不存在的对象,我们称这样的引用为dangling引用。要测试这种情况,我们应该使用 SQL的IS DANGLING语句。假设关系表department的manager字段引用了对象表中的Employee对象。我们就可以使用下面的UPDATE语句来把"dangling"引用转为空值:
UPDATE department
SET manager = NULL
WHERE manager IS DANGLING;
- 使用DEREF函数
我们不可以在PL/SQL过程语句中。(DEREF是dereference的缩写。当我们反引用一个指针时,我们就能获取它所指向的值。) DEREF把对象的引用作为它的参数值,然后返回这个引用所指向的对象。如果引用是"dangling"的,DEREF就会返回一个空对象。
在下面的例子中,我们可以反引用一个指向Person对象的引用。要注意的是,我们是从dummy表dual中取得引用值的。我们不需要指定对象表和检索条件,因为每个存放于对象表的对象都有唯一的互斥的对象标识,它是每一个指向对象的引用的一部分。
DECLARE
p1 person;
p_ref REF person;
NAME VARCHAR2(15);
BEGIN
...
/* Assume that p_ref holds a valid reference
to an object stored in an object table. */
SELECT DEREF(p_ref)
INTO p1
FROM DUAL;
NAME := p1.last_name;
END;
我们可以在后续的SQL语句中用DEREF进行反引用操作,如下例所示:
CREATE TYPE personref AS OBJECT(
p_ref REF person
)
/
DECLARE
NAME VARCHAR2(15);
pr_ref REF personref;
pr personref;
p person;
BEGIN
...
/* Assume pr_ref holds a valid reference. */
SELECT DEREF(pr_ref)
INTO pr
FROM DUAL;
SELECT DEREF(pr.p_ref)
INTO p
FROM DUAL;
...
END;
/
下面是一个我们不能在过程语句中使用DEREF函数的例子:
BEGIN
...
p1 := DEREF(p_ref); -- not allowed
在SQL语句中,我们可以使用点标志通过一个对象字段来引用它的属性,也可以通过一个对象字段的属性引用另一个属性。如果使用了表别名,那么就还能通过引用字段来访问属性。例如,下面的语法就是有效的:
table_alias.object_column.ref_attribute
table_alias.object_column.ref_attribute.attribute
table_alias.ref_column.attribute
假设我们在SQL*Plus中运行下面的脚本来创建对象类型Address和Person,对象表persons:
CREATE TYPE address AS OBJECT(
street VARCHAR2(35),
city VARCHAR2(15),
state CHAR(2),
zip_code INTEGER
)
/
CREATE TYPE person AS OBJECT(
first_name VARCHAR2(15),
last_name VARCHAR2(15),
birthday DATE,
home_address REF address, -- shared with other Person objects
phone_number VARCHAR2(15)
)
/
CREATE TABLE persons OF person
/
引用属性home_address对应对象表persons中的一个引用,该引用指向存放在其他表中的Address对象的字段。填充数据表之后,我们就可以像下面这样反引用它的引用来得到特定的address:
DECLARE
addr1 Address;
addr2 Address;
...
BEGIN
SELECT DEREF(home_address)
INTO addr1
FROM persons p
WHERE p.last_name = 'Derringer';
END;
在下面的例子中,我们可以通过引用字段home_address来找到属性street。这种情况下,我们必须要使用表别名。
DECLARE
my_street VARCHAR2(25);
...
BEGIN
SELECT p.home_address.street
INTO my_street
FROM persons p
WHERE p.last_name = 'Lucas';
END;
2、插入对象
我们可以使用insert语句为对象表添加一条记录。下例中,我们向对象表persons插入一个Person对象:
BEGIN
INSERT INTO persons
VALUES ('Jenifer', 'Lapidus', ...);
END;
另外,我们还可以使用对象类型Person的构造函数来插入记录:
BEGIN
INSERT INTO persons
VALUES (person('Albert', 'Brooker', ...));
END;
下例中,我们可以使用RETURNING子句把对象Person的引用保存在本地变量中。注意一下这个子句是如何模拟SELECT语句的。我们也可以在UPDATE和DELETE语句中使用RETURNING子句。
DECLARE
p1_ref REF person;
p2_ref REF person;
BEGIN
INSERT INTO persons p
VALUES (Person('Paul', 'Chang', ...))
RETURNING REF(p)
INTO p1_ref;
INSERT INTO persons p
VALUES (Person('Ana', 'Thorne', ...))
RETURNING REF(p)
INTO p2_ref;
END;
要往对象表中插入对象,我们还可以利用返回同样对象类型的子查询来进行操作。示例如下:
BEGIN
INSERT INTO persons2
SELECT VALUE(p)
FROM persons p
WHERE p.last_name LIKE '%Jones';
END;
拷贝到对象表person2的行被赋予新的对象标识。对象标识是不会从对象表persons中拷贝出来的。
下面的脚本创建一个名为department的关系表,其中有一个类型为Person的字段,然后向表中插入一条记录。注意如何使用构造函数Person()为字段manager提供值。
CREATE TABLE department (
dept_name VARCHAR2(20),
manager person,
LOCATION VARCHAR2(20))
/
INSERT INTO department
VALUES ('Payroll', Person('Alan', 'Tsai', ...), 'Los Angeles')
/
存放到字段manager中的新Person对象是不能被引用的,因为它是被存放在字段中(不是行中)的,也就没有对象标识。
3、更新对象
如果要修改对象表中的对象属性,我们可以像下面这样使用UPDATE语句:
BEGIN
UPDATE persons p
SET p.home_address = '341 Oakdene Ave'
WHERE p.last_name = 'Brody';
UPDATE persons p
SET p = person('Beth', 'Steinberg', ...)
WHERE p.last_name = 'steinway';
END;
4、删除对象
我们可以用DELETE语句从对象表中删除对象。要有选择的删除对象,我们就得在WHERE子句中进行指定:
BEGIN
DELETE FROM persons p
WHERE p.home_address = '108 Palm Dr';
END;