开发对象-关系数据库应用程序(第二部分)

Paul Brown
IBM Informix
2002 年 6 月
开发对象-关系数据库应用程序(第二部分)
 
  内容  
开发对象-关系数据库应用程序(第二部分)
简介
开发对象-关系数据库应用程序(第二部分)
初始实现
开发对象-关系数据库应用程序(第二部分)
关于分析失效的说明
开发对象-关系数据库应用程序(第二部分)
有关组织信息系统的说明
开发对象-关系数据库应用程序(第二部分)
结束语
开发对象-关系数据库应用程序(第二部分)

简介

这篇文章以前曾在 Informix Tech Notes 季刊上发表过。Tech Notes 文档集收集了该季刊 1991 年到 2001 年的文章。要获取自 1998 年以来或更早一些的文章的重印本,请与 Technical Support tsmail@us.ibm.com 联系

编者按:本文是该系列文章(由两篇文章构成)的第二篇文章,该系列文章是关于开发对象-关系数据库应用程序的。本文源自 Informix Press 出版的 Developing Object-Relational Database Applications 一书。第一篇文章(也就是第一部分)涵盖了数据库分析和设计方法。本文(也就是第二部分)则着重讨论了应用程序实现。

初始实现

在第一篇文章的结尾处,我们看到了如何构建一个问题域的概念模型,而您的对象-关系数据库应用程序的构建正是用来支持该问题域。该概念性模型由语义数据模型 — 和扩展 ER (Extended ER)或 UML 图 — 以及一组定义组成,这组定义描述了该模式内所用到的每个域(或对象类)的结构和行为。在这一部分中,我们将描述所要遵循的一系列步骤,从而将这一分析结果变成可用的软件。

简单地讲,这个问题就是要将图变成有效的代码。这需要一个两步过程。首先,需要使用前面分析的结果来设计正确、完整的代码体。其次,需要评估该代码以确保技术设计和硬件达到系统的功能、性能和可靠性目标。若不能达到,请调整设计或体系结构以符合您的要求。

关于分析失效的说明

在深入进行任何开发工作以前,需要给您一个提醒。软件开发人员的一条经典规律是:虽然做一些分析很有帮助,但过多的分析反而会让人找不到方向。试图构建全面的概念性模型常常徒劳无益。

首先,如果您已经研究了某个主题很久,您就会发现其内容简直无穷无尽。多了解一些可能并不是什么坏事,但构建大多数信息系统却受时间和材料的限制,于是从容不迫地了解所有的内容近乎于一种奢侈。等到了解了所有事情才开始编码将导致项目开发工作不能按计划完成。

其次,问题域一直在变化。曾经正确的东西一星期后可能就不正确了。用户会举棋不定、改变主意、相互争论、甚至会提出前后截然不同的要求。将他们的实际情况与您的图作比较是非常困难的。这会导致反复修订,因为您要努力地应付不断变化的需求。

“分析失效”— 对于其变化速度是分析人员很难跟上的问题所进行的无休止的研究 — 描述了某些项目所陷入的状态。

如同 Don Knuth 的著名论断所说的那样,创建计算机程序就象在计算机内构建一个世界模型。模型是一种抽象:它们通过只考虑现象的重要或实质方面而忽略其它方面来简化复杂现象。这种诀窍在于要确保您所专注的问题部分是正确的,并且您的模型有足够的说明能力。

因此,做最少的分析、直接构建系统的最初版本是个不错的主意。然后,将此软件同实际情况做比较。计算机程序同实际行为之间的差异很容易发现,这些差异表明模型需要细化。而且,软件成功与否的最终衡量标准是其实用性。对于不完善的工作系统,可以对其进行修改和改进。

精练的图是必须的也是有用的。但它们最终对您的用户没有帮助。

设计方法概述

以下是将概念性模型变成数据库实现要遵循的步骤:

  1. 根据域/对象分析实现类型系统。为此,请使用用户定义的类型和用户定义的功能机制。
  2. 创建初始的、关系设计,它使用该类型系统来记录描述问题域状态的事实。这包括编写“create table”语句。
  3. 规范化模式以确保它没有含混不清的地方。这可能导致要调整您的表设计。
  4. 用生产规模的数据来填充模式,对该数据库实现一组工作负载查询以测量其性能。这涉及编写运行于数据库模式上的 DML 查询。
  5. 根据执行上面步骤中所了解到的情况,重新考虑在步骤 1 到步骤 4 中所作出的设计决定。

下面几页将更详细地讲述各个步骤。

实现类型系统

ORDBMS 数据模型为实现新对象类提供了多种技术。在图中,我们为您的选择做了简要概述:

图 1. 具有顺序支持的 UML 类图示例
开发对象-关系数据库应用程序(第二部分)

在这一节中,我们更详细地探讨各种 UDT 机制的利与弊。

内置数据类型
即便使用 ORDBMS 产品,内置类型有时仍然是最好的选择。对于较成熟的 SQL-92 类型来说,尤为如此。对于记录 DATE 和 DATETIME 值来说,没有比它们更好的了。

然而,SQL-92 支持这些类型的方式却有几处不足。例如,SQL 管理 INTERVAL 数据的方法复杂且不完整,其不完整性可以证明。有些从事移植应用程序的开发人员经常表达他们希望 Informix 产品能够实现其它产品某些功能的愿望。

因此,即使您选择将使用的数据类型严格限制为来自 SQL-92 的标准数据类型集,创建将新的、所期望的功能嵌入到 SQL 查询语言的用户定义的例程仍然非常有用。

DISTINCT 类型
您经常会发现新类型的结构和行为同某个现有的类型几乎完全相同。例如,在我们的模式中,我们标识了一个称之为 Quantity 的域。Quantity 的实例十分类似于 SQL-92 INTEGER 类型,唯一不同是它们不能为负数。利用内置 INTEGER 的成熟和性能,同时再用数据库来确保没有人会无意中输入负的 Quantity 值,这是一种理想的做法。当然也可以使用 DISTINCT 类型并修改 CAST 函数以强制执行这条正确性规则来实现这一点。

例如,请参阅下图 2

图 2:使用 CAST 来强制数据完整性的 DISTINCT 类型示例


--

-- This script creates a new DISTINCT type called

-- Quantity using the built-in SQL-92 INTEGER type

-- In order to check that the Quantity instance is

-- a non-negative value, I overload the CAST with my

-- own function.

--

CREATE DISTINCT TYPE Quantity AS INTEGER;

--

CREATE FUNCTION INTEGER2Quantity ( Arg1 INTEGER )

RETURNS Quantity

      IF ( Arg1 < 0 ) THEN

            RAISE EXCEPTION -746,0,

 "Error: Quantity must be non-negative";

      END IF;

      RETURN Arg1::LVARCHAR::Quantity;

END FUNCTION;

--

DROP CAST ( INTEGER AS Quantity );

CREATE IMPLICIT CAST

( INTEGER AS Quantity WITH INTEGER2Quantity );

这段代码中有几条重要细节。首先,请注意函数的返回是如何将参数值强制类型转换成 Quantity 类型的,但这样做却首先将其变成 LVARCHAR。这在 SPL 函数中是必须的,因为直接从 INTEGER 强制类型转换成 Quantity 将导致 ORDBMS 反复递归调用同一个函数,直至内存耗尽。通过将这一强制类型转换函数作为外部函数实现,C UDR 将消除执行这一额外的处理步骤的需要。

同样,请注意该示例中 IMPLICT 强制类型转换的使用。缺省情况下,创建 DISTINCT 类型时,在它们和其父类型间进行了 EXPLICIT 强制类型转换。EXPLICIT 强制类型转换可能导致您要在查询中使用两个分号,从而使查询变得很混乱。IMPLICIT 强制类型转换则没有这些要求。

在我们的‘Boxes-R-Us’概念性模型中所标识的域中,使用这一技术可以很好地处理 Quantity、各种标识符以及具有固定小范围值的域 — 诸如 Address 对象中使用 United States 集合。

ROW TYPE
对于某些复杂类型(如,具有内部结构的域/对象),ROW TYPE 是一种理想的机制。使用类图,您可以通过为类的每个属性创建一个元素来实现对应的 ROW TYPE。UML 类图不能直接表示它,但有了 ROW TYPE 机制,您可以用 NOT NULL 约束来指定类型的哪些元素是必需的以及哪些是可选的

在下图 3 中,我们给出了 ROW TYPE 的实现,它对上一篇文章中描述的 BirthDay 域进行建模。为了使本文不依赖其它文件,我们重复了前面开发的 UML 类图。图中包含了实现该类型行为的代码:构造器以及用于比较两个生日是否相等的逻辑。我们在后面一节中详细讨论该代码。

图 3:实现 Birthday 对象类的 ROW TYPE


--

-- This script illustrates how the Birthday Object

-- Class in Figure 11. might be implemented using the ROW

-- TYPE.

-- 

CREATE ROW TYPE BirthDay (

 MMonth INTEGER NOT NULL,

 DDay INTEGER NOT NULL,

 FromLeapYear BOOLEAN NOT NULL

);

GRANT USAGE ON TYPE BirthDay TO PUBLIC;

--

-- Constructor

--

-- Note that this example uses a combination of built-in

-- SQL-92 expressions { MONTH(), DAY() } and another

-- function whose implementation we do not show here.

--

CREATE FUNCTION BirthDay ( Arg1 date )

RETURNING BirthDay

 DEFINE nYear INTEGER;

 LET nYear = YEAR(Arg1);

 RETURN ROW (MONTH(Arg1),

 DAY(Arg1),

 IsLeapYear(nYear))::BirthDay;

END FUNCTION;

GRANT EXECUTE ON FUNCTION BirthDay ( date ) TO PUBLIC;

--

-- Compare() support function, used as the basis for a

-- set of other functions, like Equal(). Note that the

-- ORDBMS will not use this function for sorting or

-- indexing a column of this datatype.

--

CREATE FUNCTION Compare ( Arg1 BirthDay, Arg2 BirthDay )

RETURNS INTEGER

 DEFINE nRetVal INTEGER;

 LET nRetVal = -10;

 IF ((NOT(Arg1.MMonth = 2 AND Arg2.MMonth = 2)) OR

 ((Arg1.FromLeapYear AND Arg2.FromLeapYear) OR

 ((NOT Arg1.FromLeapYear ) AND

 ( NOT Arg2.FromLeapYear ))) ) THEN

 IF (( Arg1.MMonth < Arg2.MMonth ) OR

 (( Arg1.MMonth = Arg2.MMonth) AND

 ( Arg1.DDay < Arg2.DDay))) THEN

 LET nRetVal = -1;

 ELIF (( Arg1.MMonth > Arg2.MMonth ) OR

 (( Arg1.MMonth = Arg2.MMonth) AND

 ( Arg1.DDay > Arg2.DDay))) THEN

 LET nRetVal = 1;

 ELSE

 LET nRetVal = 0;

 END IF;

ELIF ( Arg1.FromLeapYear ) THEN

 IF (( Arg1.DDay = 29) AND ( Arg2.DDay = 28 )) THEN

 LET nRetVal = 0;

 ELIF ( Arg1.DDay < Arg2.DDay ) THEN

 LET nRetVal = 1;

 ELSE

 LET nRetVal = -1;

 END IF;

 ELIF ( Arg2.FromLeapYear ) THEN

 IF (( Arg2.DDay = 29) AND ( Arg1.DDay = 28 )) THEN

 LET nRetVal = 1;

 ELIF ( Arg1.DDay < Arg2.DDay ) THEN

 LET nRetVal = -1;

 ELSE

 LET nRetVal = 0;

 END IF;

 END IF;

 IF ( nRetVal = -10 ) THEN

 RAISE EXCEPTION -746, 0,

 "ERROR: Asked to Compare an invalid Birth Date";

 END IF;

 RETURN nRetVal;

END FUNCTION;

ROW TYPE 有重大的缺陷。在此,您无法使用 ROW TYPE 构建索引,也无法对它们进行排序或者在联合查询中使用 ROW TYPE。这限制了 ROW TYPE 作为 UDT 机制发挥作用,但对于实现诸如我们在前面介绍的 Birthday 类型的应用程序级别数据类型,它们仍然非常有用。

在概念性模型里所标识的域/对象中,各种 Address 是作为 ROW TYPE 来实现的理想候选。这些域/对象在继承层次结构内相互关联这一事实进一步表明使用 ROW TYPE 是合适的。

Java 类
Java®-in-the-server 是最近才被添加到 Informix Dynamic Server.2000 产品中的。但它却是实现某些类型的域/对象的一种非常有效的机制。Java 的最大优点在于其移动性;同一个 Java 类既可以部署在 ORDBMS 服务器内,也可以部署在中间件应用程序服务器,甚至还可以部署在运行于浏览器里的 applet 内。因此,Java 是实现必须部署在 ORDBMS 之外的任何东西的理想方法。

例如,请考虑 PersonName 和 Phone_Number 域/对象。除了可以在数据库中存储和查询这种类型的实例之外,如果同一个对象可以表示自己以供最终用户在客户机程序中进行修改,那将非常有用。Java 语言的面向对象特征使得创建这样的集成组件成为可能。而 Java 的动态链接特性使得可以部署这一组件。

OPAQUE TYPE
OPAQUE TYPE 是最难以实现的 UDT。但 OPAQUE TYPE 也是最灵活的机制,并且它们提供了最佳性能。由于这些原因,大多数商业 DataBlade™ 产品广泛使用 OPAQUE TYPE。就 ORDBMS 而言,OPAQUE TYPE 数据的内容只不过是字节数组。可以使用查询语言在查询中将函数名称同列名称和表名称绑定在一起来调用类型的行为。

在‘Boxes-R-Us’示例中,将诸如 Geo_Point、Mass 和 Size 之类的类型作为 OPAQUE TYPE 来实现是个不错的主意。这些类型都涉及相对复杂的逻辑,而这些逻辑涉及由简单类型组成的结构。而且,该逻辑实现对这些对象实例进行排序和索引的支持操作,而由于会频繁调用这些逻辑,这就使得类型必须能够很好地执行。

实现行为:

虽然可以使用任何过程语言来实现用于任何类型的、由用户定义的数据类型(内置的、DISTINCT、ROW 和 OPAQUE)的函数,但某些语言却更适合于某种特定类型的机制。例如,存储过程语言(SPL)非常适合于实现 ROW TYPE 的行为,因为它具有 ROW TYPE 机制的简单性。而另一方面,C 是将逻辑应用到 OPAQUE 类型的首选方法。

确定使用何种语言是个复杂的问题。基本原则如下:

  • 如果用户定义的函数含有 SQL 回调(在其逻辑中嵌入查询),那么无论使用什么语言来实现它,与运行查询有关的开销几乎总是执行函数过程中最耗费计算能力的方面。这意味着应该总是使用 SPL 来创建包含 SQL 回调的函数,因为它是到目前为止最简单的技术。
  • SPL 是最易于使用的语言,这意味着它是开发新功能的最快方法。Java 是一种比 SPL 更复杂的语言,因此用 Java 开发逻辑要花费更多时间。同时,由于 Java 具有大型实用程序库,这使得它更易于开发复杂扩展。C 是最低级别的编程语言,因此比 Java 或 SPL 更难使用。
  • 调用 C 和 Java 函数的查询可以并行运行。调用 SPL 逻辑的查询却不能这样。
  • Java 代码是可移动的。正确实现的 Java 类既可部署于 ORDBMS 内,也可部署于中间件应用程序服务器内,还可以部署于远程运行的 applet 内。然而,C 和 SPL 却不能这样。

开发 ORDBMS 应用程序中一个重要目标是要将投入的工作量减到最少。这意味着,最好尽可能地重用组件并且使用最简单的且实际可行的技术。最初,可以将 SPL 用于特定于应用程序的新型扩展。作为测试工作的一部分,您能够标识任何正在执行的类型,并且,如果有必要,可以使用更有效、但也更耗时间的机制来重新实现它们。

作为替代的各种 UDR 机制的性能
评估不同机制的运行时性能是个复杂的问题。在下图 4 中,我们给出了一个简单实验的结果,对调用 UDR 的开销以及各种语言的运行时性能进行了评估。

我们通过实现一个仅仅能立即返回其参数值的小 UDR 来衡量 SPL、Java 和 C 的调用开销。为了估计各种语言的运行时性能,我们实现了一个用于确定某个整数参数是否为质数的算法。该算法只是按递增次序循环遍历每个整数,寻找能够整除该参数值的整数,当循环至该参数值的平方根时就停止循环。为了更加真实,每个 UDR 都包括冗余内存分配和字符串操作步骤。

这个算法的思想是,一般来讲数越大执行该算法的时间也就越长。但数越大,它是质数的可能性也就越小,这使问题变得更复杂。尽管如此,实验结果仍然能揭示一些问题。

为了获得下图中的测量结果,我们对几组数运用了这些函数,每组数中所含数的数目相等(都是 10,000),但数的大小却在 1 和 10,000,010,000 之间变动。这样做的思想是,确定较大的数是否是质数要花费更多计算资源。

图 4. 比较三种 UDR 机制的运行时性能
开发对象-关系数据库应用程序(第二部分)

比较结果是 C 和 SPL 的调用开销要比 Java 的调用开销低得多,并且显然 C 具有最佳运行时性能。但 Java 的出色运行时性能意味着:一旦 UDR 复杂到了某种程度,Java 就会比 SPL 做得好。注:“SPL 质数性能”的最终值是 2401 秒,为了使图上的信息清晰,我们没有显示它。

域、对象和 UDR/UDT 开发
开发嵌入到 ORDBMS 中的新对象/数据类型时,您不可能预见能用于该对象/数据类型的所有方式,牢记这一点是很有帮助的。最初的关系 DBMS 产品的最大优点可能在于它们允许用户回答在运行时才会被问到的特别问题,这些问题在设计时根本无法预见。类似地,对于新类型,为其实现您认为用户可能需要的行为是个不错的主意。也就是,着重于每次实现一种数据库类型,将其作为一种自主、完整且自包含的对象定义。

例如,考虑标识为属于概念分析的 Mass 对象。在下图 4a,我们演示可能如何使用该类型。

图 4a. 比较三种 UDR 机制的运行时性能
开发对象-关系数据库应用程序(第二部分)

一位加拿大客户想要一个能装‘3 千克’东西的箱子


SELECT P.Id,

 P.Capacity

 FROM Products P

 WHERE P.Capacity > '3 KG'

 ORDER BY P.Capacity ASC;

图 5. 演示 SQL 中 Mass 的使用的样本数据和查询
开发对象-关系数据库应用程序(第二部分)

实现 Mass OPAQUE TYPE 时,最好实现与更常用的数联系起来的所有功能,即便您所做的分析并未说明您需要这些功能。对此的一种思考方法是认为 Mass 是模式(pattern)的实例:您可能称之为“数字”模式。除了在该查询中所见到的运算符之外,数字还可以使用所有数学运算符和建立索引支持。相比之下,数学运算符对于 Dimensions 类型没有意义,因为它遵循另一种模式。

请考虑在其它应用程序可以如何使用新类型,以及数据库以分布式方式部署时可以如何使用新类型。

SQL 查询和 UDR
实现 Mass 类型的一种方式将是用表来保存各种不同质量单位(千克、克、盎司和磅)以及它们相互之间的换算率。然后比较 Mass 数据类型实例的用户定义函数可以对该表使用 SQL 查询。但这种方法有许多缺点。

首先,查询是代价非常高昂的操作,在该示例中根本没必要。换算率不会改变,新的 Unit 也极少。在换算逻辑内将换算率硬编码为静态数据结构可以改进性能和扩大可伸缩性的范围。其次,使用表来解决这一问题会导致数据库中出现了同任何其它事情都没有明显关系的表。

因此,避免将 SQL 查询放入 UDR 是个不错的主意。如果要把 SQL 放入 UDR,请避免使用‘C’或 Java。对于含有 SQL 的 UDR,其运行时代价几乎全部花在运行查询上了;跟使用哪种语言没有什么关系。

但另一方面,完全禁止在行为函数中使用 SQL 是不切实际的。有些换算率确实随时间变化而变化;象 Currency 就是那样的。加元对美元的汇率每天都在变。处理汇率的最有效方法是创建一张表来存储汇率,并在处理换算的逻辑中包含 SELECT 查询。虽然这样做降低了数据库的速度,但有必要确保查询结果正确。

使用 DataBlade
您经常会发现:可以在 DataBlade 中找到在您的模式(schema)中需要使用的一些数据类型。您可以购买而无须自己构建一些功能,在我们的示例中,Geographic_Point 和 Document 对象是这些功能最明显的示例。一般来说,这是个不错的主意。当您应用程序中的代码和运行在成千上万其他人的应用程序中的代码相同,以及该代码最初由某些特定技术领域的专家编写而成时,该代码可能将只有极少的错误并且性能将比您自己编写的代码要好得多。

而且,Web 上有许多地方可以找到有用的“bladelet”扩展。通常,它们都十分完整,都带有完整的源代码,这使得可以容易对它们进行修改。它们是极有价值的示例,演示了服务器应用程序编程接口(Server Application Programming Interface (SAPI))和 UDT/UDR 设计的更为复杂的方面。在下图 6 中,我们列出了一些有用的网站。

图 6. 包含预先封装的扩展和示例代码的 Web 资源
开发对象-关系数据库应用程序(第二部分)

最后,对于概念分析阶段所标识的每个对象/域,都有与之对应的在数据库中表示的用户定义类型以及一组用户定义的函数。除了数据模型中的域以外,看一看那些工作负载查询以及您所标识的任何应用程序级的业务过程也是个不错的主意。数据库表中永远也不需要使用您创建的类型。但这些类型在查询或用脚本编制的业务过程中可能仍然有用。

设计模式

由于已经在模式中为每个低级别域实现了至少一个原型,所以下一步是使用这些类型来构造一组基于高级别 E-ER 模型的表。

下面,我们描述要遵循的步骤。在图 7 中,我们展示了模式创建脚本中的大部分,该脚本是对前一节中定义的概念性模型应用下面这些步骤而产生的。

  1. 对于不参与继承层次结构的每个实体,以相同名称创建一个对应表,以及一组与实体属性相匹配的列。对任意键和列的约束进行标号。Customers 表说明了这一点。
  2. 对于继承层次结构中的实体,创建与每个实体结构相匹配的 ROW TYPE(您可能已经这样做了),它根据层次结构适当地进行组织。接着,创建一组对应表,使用 ROW TYPE 来定义它们的结构。
  3. 通过为每个外键引用添加列来扩展表定义。例如,每个分部制造一种产品,产生一个外键。要对此进行建模,则可在 Branches 表中添加一列以保存 Products 表的主键列的值。
  4. 对于每个多价关系,创建一个表,由参与表的每个主键组成列,并为每个关系的属性添加列。象多个 Employees 和 Branches 之间的关系就是这样处理的。

为什么不为所有表创建 ROW TYPE 呢?如果数据库模式十分稳定,那么这应该是个好主意。实体通常与一些应用程序级别对象相对应,而这些对象通常与业务过程行为相关联。但是 ROW TYPE 和表之间的关系很复杂,不能轻易修改类型使得改进成确定类型的表很困难。只有当表不是用类型创建时,ALTER TABLE 才允许您修改列。不过,对于继承层次结构,强制使用 ROW TYPE。

这种转换的结果是产生了创建一组表和约束的数据定义语言脚本。熟悉 RDBMS 开发的读者将会注意到,实际上,创建表需要进行其它决定:如何组织磁盘资源上的数据;如何对表设置权限和角色;以及通过禁用日志记录和完整性约束,如何使大批装入尽可能变得简便和有效。本文中,我们忽略了这些重要问题,以便集中讨论设计问题。通常,ORDBMS 扩展性并未对您应如何处理这些问题产生影响,并且在 RDBMS 数据库获得的经验教训通常同样也可以应用于 ORDBMS 数据库。

在下面的图 7 中,我们展示了由上面概括的步骤产生的 DDL 脚本的子集。

图 7. 模式创建文件的子集
开发对象-关系数据库应用程序(第二部分)

读者应该注意这个模式中的几个细节。首先,观察我们是如何严格执行强类型规则的。从列中使用的类型来看,图 7 中模式的语义是非常显而易见的。强类型用于强调表之间可能存在的关系。例如,因为我们知道列 Customers.Location 是一个 GeoPoint,而且我们也知道 Sales.Region 列是一个 Geo_Polygon,所以您可以知道编写这些表之间的查询来推断新信息是可能的。或者可以知道 Product.Price 和 Sales.Quota 在语义上是相关的,因为它们共享相同的数据类型。

其次,请注意该模式将 SERIAL 类型用作代替键的方法。实际上,尽管 SERIAL 和 INTEGER 类型就其物理结构而言是相同的,并且尽管 ORDBMS 提供了内置功能来支持对比 SERIAL 和 INTEGER 实例的查询,但是创建同样的类型需要一些技巧。Product_Id 就是 INTEGER 的一个 DISTINCT TYPE。

键和约束
首先,先对键作一说明。严格地说,一个键是一列(或一组列),其中任意给定行的键值将不会出现在另一行的相同列中。另一种说法是键列中任何两个键都是不同的。对于 SQL-92 数据库,任何内置数据类型都可以用来定义键列,因为它们都拥有一个内置的相等表达式。并且基于效率考虑,DBMS 可以为所有的内置类型构建一个索引。

根据其实质,任何拥有用户定义函数 Equal() 的对象类/数据类型都可以用于定义表中的键列。例如,假设 Product_Num 的定义需要比 INTEGER 更复杂的数据类型。许多企业使用“智能的”部件号,其中有关部件或产品的信息就在构成标识符的号码或字母序列内进行编码。对产品号强制进行完整性约束是极其有用的,尤其是在简单字符串匹配不能满足要求的情况中。

扩展性影响了键主题的其它方面。在我们的 Boxes-R-Us 模式中,更详细地研究了 Works_At 表。在该模式的所有表中,它是唯一一个没有主键的表。这是因为其唯一性比其它表更复杂。严格地说,为了维护表的完整性,当出现以下情况时,两个行 R1 和 R2 是冲突的:

图 8:临时受约束的主键


 ( ( R1.Employee = R2.Employee ) AND

 ( R1.Branch = R2.Branch ) AND

 ( Overlaps ( R1.Duration, R2.Duration ) )

 )

这种约束不能表示成常规的 PRIMARY KEY,而且必须强制使用 TRIGGER。

处理 COLLECTION
使用 COLLECTION 还是创建辅助表是个难以回答的问题。一方面,从数据建模的观点来看,COLLECTION 通常很吸引人,因为它们能简化模式设计并且更接近于表示用户级别的观点。另一方面,COLLECTION 会引起性能和数据完整性问题。例如,当前建立索引技术无法使您快速找到所有包含特殊值的 COLLECTION 实例。而且很难确保 COLLECTION 中的值与某种完整性规则相符。

通常,当非第一范式属性包含拥有很小基数的域(只有几个可能的值,且强制作为类型定义的一部分)的实例时使用 COLLECTION。例如,在我们的 Boxes-R-Us 数据库中,Products 表包含一个列出了可用于产品的颜色的列。碰巧颜色的种类十分有限,所以我们可以使用枚举模式对这个域建模,并以这种方式强制其正确性。

另一方面,可以令人信服地对 Sales、Products 和 Customers 之间的关系建模,作为这些实体中任意一个非第一范式属性。但那将是个巨大的错误。嵌套的关系模式听起来象是个好主意,但是经验表明它是充满危险的。主要危险在于您的底层模式最终只反映单个观点,但首先在您脑海中出现的是该模式应该根据支持多个最终用户观点的需要进行设计。改进它以满足另一个用户的需要是非常困难的。

处理继承
继承是另一个诱人的概念,因为人类生来喜欢对事物进行分类。我们喜欢根据事物的结构及其与其它事物的关系来进行分类。基于这些原因,继承在许多情况下都是一个有用的建模工具。但是它不是对每一个事物都适用的建模方法。

大型继承层次结构(所谓“大型”是根据包含的数据种类而变化的)会导致性能下降,特别是当您编写涉及层次结构的连接查询时。查询的执行时间不是问题。但是,解析和优化涉及大型继承层次结构查询的复杂性需要大量的 CPU 和内存资源。而且,ORDBMS 表维护工具的相对不成熟阻碍了开发人员对继承的广泛使用。

将您对任何数据建模特性的使用限制在有意义的情况下,是合理的软件工程实践。例如,在 Boxes-R-Us 模式中,使用继承层次结构对各类雇员进行建模就是对该技术一种很好的使用。与此相反,对各类纸盒中的每一个都作为产品下的一个子表进行建模就不太合理。

规范化和 ORDBMS 表

“规范化”这个词使许多数据库开发人员寒心。这个概念通常被解释为复杂规则和技术术语的明显随意的集合。但是就其本质而言,规范化的目标十分简单:确保每个事实都正确地保存一次。要应用规范化算法,数据库模式需不断修正初始的、不完善的设计,以消除可能出现的某种异常(anomaly)

规范化是一组将数据库开发与其它计算机编程方法区别开来的正规技术。规范化数据库模式没有什么好的技术理由 — 比如改进性能、可伸缩性或效率。已从根本上完全证明的是:信息系统首先也是最重要的一点是必须保证其正确性。在错误的方向以两倍的速度运行比保持原地不动更糟糕。

本节中,我们简要概述了规范化概念。ORDBMS 数据模型几乎没有对这个许多读者以前已经听说过的概念作任何添加。包含该摘要是为了使本文成为一个独立的整体。因为规范化是一个非常大的主题,所以我们在这里只讨论基本内容。

异常
规范化的目的是试图使数据库避免某些异常的可能性。当对模式的编写操作产生不期望的或无意的结果时,数据库模式就会有异常。因为对象-关系数据库旨在记录有关问题域中对象的事实,而这些无意的结果与其它事实相关,并不直接受初始行为的影响。

本节中,我们将介绍并演示三种我们希望避免的异常。

请考虑下表图 9,它是存储有关产品和分部的事实及它们之间关系的一种方法。在这个假设的表中,主键是 Product_Id 和 Branch_Id 的组合。

图 9. 演示异常的假设表
开发对象-关系数据库应用程序(第二部分)

  1. INSERT 异常

    请考虑如果我们试图记录有关新 Branch 的事实,那么在这张表中会发生什么。这需要我们同时记录有关新 Product 的事实。但是该信息可能是未知的,所以不能完成 INSERT。我们称之为 INSERT 异常。

  2. DELETE 异常

    现在,请考虑如果 Boxes-R-Us 关闭了“St Louis”分部,那么会发生什么。这需要我们从该表中删除对应的行。但是这样做同时也除去了存在“Small Pizza"”产品这一事实,从而导致了不希望出现的负面影响。“Small Pizza”仍然在数据库的其它地方被引用,所以除去该信息是个严重的问题。我们称之为 DELETE 异常。

  3. UPDATE 异常

    最后,请考虑如果 Boxes-R-Us 要更改它们“Shoe”盒的设计,那么可能会发生什么。如果“Sacramento”的经理进行了修改,但是“Houston”的经理却不知道,那么我们就碰到问题了。更改一行而不更改其它行会导致数据库中出现不一致的情况。我们称之为 UPDATE 异常。

当然,该示例的整个要点在于不应以这种方式对有关分部的事实、有关产品的事实以及有关它们之间关系的事实进行建模。规范化的目的是当出现这些情况时能够检测到它们,然后修改设计将其排除。应用规范化规则产生了象我们在图 7 中所展示的那样的模式。

规范化基础知识
规范化中的一个重要概念是函数相关性。这一思想来自关系型理论,它描述了关系/表中属性之间的关系。例如,存在一个产品列表,如果您有一个有效的 Product_Id 值,那么您可以明确得到有关产品的其它信息:名称、物理尺寸、质量等等。请注意反之不成立。两种不同产品可以有相同的质量,尽管它们有截然不同的标识。

Boxes-R-Us 模式包括我们可以选择用来作为函数相关性的其它情况。例如,Customers 表拥有 DeliveryAddress 和 Geo_Location 列/属性。从某种意义上说,地址决定位置。如果给定街道地址和邮政编码,那么就可能计算出对应的纬度和经度对。而且事实上也存在精确执行该操作的软件。

当函数相关性可以用用户定义函数来计算时,不存储计算的值是个好主意。象函数索引那样的技术可以解决性能问题,而且以这种方法使用函数节省了空间。另外,如果运行该函数的开销非常大,那么存储该值也是有意义的。

规范化算法检查关系/表集合中的函数相关性。它们使用该信息以确定何时存在异常。它们甚至能指出要进行怎样的更改(分解表,并将属性从一个表移至另一个表)来消除问题。

规范化的目标
我们没有对规范化算法进行完整描述,而是描述了规范化的合理目标。图 7 中的所有数据库表(Works_At 表除外)都属于所谓的 Boyce-Codd 范式(BCNF)。在形式上,对于每个函数相关性,当且仅当函数相关性左边的值(Product_Id、Customer_Id 或者可能是 MailAddress)是一个键时,该表才符合 BCNF。相同原理更精练的说法是:“属性值只能通过使用键(键的整体)进行访问”。

在许多计算机辅助软件设计工具中内置了规范化算法。这些工具以一组反映概念性模型的图表作为开始,可以生成所期望的任意级别的规范化 DDL。但是这些算法实际上只相当于在涉及键和函数相关性的模型中捕捉到的信息。

装入和测试

    及早测试并经常测试是在任何一种软件开发中都应遵守的最有可能的建议。测试有助于识别概念性错误、设计缺陷、代码错误以及您没有注意到的任何错误。开发进度通常将测试和验证作为开发过程的最后任务,而不是将它们集成到其它任务中,这是非常令人惊讶的。

幸运的是,与需要构建详细的装置代码以测试模块的其它编程系统不同,在对象-关系 DBMS 环境中不仅可以部署用户定义函数,还可以作为测试装置。

扩展的单元测试
彻底的扩展测试对数据库的顺利操作来说是必需的。尽管引擎的调试工具还不成熟,但是 ORDBMS 的本质使正确性和性能测试相对简单。遵循某一个用于您的用户定义类型的设计原则还有助于确保向类型提供的行为集合是对称的 — 例如,每个构造器函数都有相应的打印函数。

对于数值范围测试 — 多次运行扩展以检查内存泄漏等 — 使用 DBMS 查询语言的连接能力是很有用的。例如,在下图 10 中,我们展示了一个查询的概要来测试 GreaterThan(Mass, Mass) 用户定义函数。这个查询将执行大约 2000 次的 UDR。

图 10:测试 GreaterThan 用户定义函数一个方面的查询


SELECT Mass(Q1.Num,U1.Val) AS Less,

 Mass(Q2.Num,U2.Val) AS More

 FROM TABLE(SET{1.0,10.0,100.0,1000.0,10000.0,100000.0

  }::SET(DECIMAL(10,2) NOT NULL)) Q1 ( Num ),

 TABLE(SET{'G','gram','OUNCE','OZ','Ounce','POUND','LB'

  }::SET(LVARCHAR NOT NULL)) U1 ( Val ),

 TABLE(SET{1.0,10.0,100.0,1000.0,10000.0,100000.0

  }::SET(DECIMAL(10,2) NOT NULL)) Q2 ( Num ),

 TABLE(SET{'G','gram','OUNCE','OZ','Ounce','POUND','LB'

  }::SET(LVARCHAR NOT NULL)) U2 ( Val )

 WHERE Mass(Q1.Num,U1.Val) > Mass(Q2.Num,U2.Val);

该示例的思想是您可以使用查询语言可能的组合以一个表达式生成数量巨大的单个测试。对几个用户定义函数的测试可以组合成一个表达式,并且如果您密切关注引擎打印的日志文件的消息,那么就会获得象函数中内存泄漏和某些代码错误的问题报告。

负面测试(您将参数传递到用户定义的函数中,使它们生成一个异常)需要不同的方法。这些测试需要您使用 EXECUTE FUNCTION 语法,并传入函数参数值以便来要么故意引发一个错误,要么执行每个参数可以接受的极值。

工作负载查询的测试
在您完成了模式设计之后,尽早在数据库中装入接近预期生产规模的一组数据是一个非常好的主意。然后,通过使用能体现预期工作负载的查询,您可以对数据库进行操作。可以使用图 10 中介绍的相同技术来创建大量数据值。例如,下图将成百万行数据填入 Products 表中。

图 11:将 SELECT 的内容 INSERT 以填充 Products 表


INSERT INTO Products

( Name, Dimensions, Capacity, Available_In, Price )

SELECT MakeString( Random(1000), 24 ),

 Random_Dimensions(),

 Random_Mass(),

 Random_Color_Set(),

 Random_Currency()

 FROM TABLE(SET{0,1,2,3,4,5,6,7,8,9}) N1 ( Num ),

 TABLE(SET{0,1,2,3,4,5,6,7,8,9}) N2 ( Num ),

 TABLE(SET{0,1,2,3,4,5,6,7,8,9}) N3 ( Num ),

 TABLE(SET{0,1,2,3,4,5,6,7,8,9}) N4 ( Num ),

 TABLE(SET{0,1,2,3,4,5,6,7,8,9}) N5 ( Num ),

 TABLE(SET{0,1,2,3,4,5,6,7,8,9}) N6 ( Num );

MakeString( INTEGER, INTEGER ) 是该查询中的第一个用户定义函数,它创建了随机分布的字符串,其第二个参数是字符串的长度。其它每个函数执行类似的任务。

基于这个测试的结果,您可以决定您下一个任务应该是什么。正在执行的工作负载查询可能指出您应更改用户定义函数的实现,或创建一个索引,或重新检查有关您如何组织数据存储的决定。合成的数据极少是完美的。但它会向您提供关于一些问题的有价值的及早提示。

有关组织信息系统的说明

在本系列的第一篇文章中,我们指出,通过对信息系统采取更为全面的查看,开发人员可以充分使用他们的对象-关系 DBMS 产品。对象-关系(或可扩展)数据库管理系统允许开发人员在数据管理框架内嵌入大量的信息系统逻辑,而且它们允许开发人员使用声明型查询语言来组合逻辑和数据。

但是 ORDBMS 还有其它优点。开发人员可以添加新的功能,而且无须停止正运行的系统就可以更改或升级现有逻辑。这更改了管理信息系统的方式。通常情况下,软件升级是定期发行的。这是由于常规编程语言技术解决已编译模块之间的引用所使用的方法引起的,因为这些模块被链接到了一个可执行文件。但是,在 ORDBMS 中,链接是递增性的,因此不够灵活。

这样做的结果是通过使用 ORDBMS 作为平台而构建的信息系统在其开发中是比较有组织的。通过在运行的系统内更改或添加单个模块,可以解决错误或调整范围。事实上,将 ORDBMS 看作不只是一个置于软件栈底部的数据资源库是有积极意义的。

另外,ORDBMS 在单一框架内灵活地组合逻辑和数据的能力使它与传统的 DBMS 或中间件软件有明显的不同。这些能力将它归为一个新的类别。而且部署使用这些有组织或发展原则的信息系统是很有用的,它能帮助您更好地处理最终用户不断变化的日常需求。

结束语

本系列的第一部分描述了适合于在象 Informix Dynamic Server.2000 那样的对象-关系 DBMS 中使用的概念性建模的方法。Informix Dynamic Server.2000 的功能可以被认为是关系模型的一个实现,它比构建用来支持 SQL-92 的关系 DBMS 产品更接近于最初思想的精髓。本质上,Informix Dynamic Server.2000 引擎允许您构建一个数据模型来存储有关问题域中重要对象的相关事实。

关系 DBMS 使用的所有方法和技术 — 象实体-关系图、规范化理论以及甚至是物理调整的概念 — 同样适用于对象-关系 DBMS 中。但是与 RDBMS 产品不同,ORDBMS 提供了更好的数据建模,因为它允许开发人员对使用用户定义类型的对象和用户定义函数进行建模。概念性分析的结果是一组表示问题域的概念性视图及其内部对象的结构和行为的图表。

在这第二篇文章(也是最后一篇中),我们看到一组用于将这些图表转换成有效代码的技术。首先,您开发了与在概念性分析阶段已标识的一组域或对象相对应的一组用户定义的类型和函数。接着,将这些对象组合成数据库模式 — 一组表和约束,其中表的列中使用了新类型。规范化理论设法确保数据库模式不包含任何出错的可能,它可以应用到对象-关系数据库中。

最后,我们指出对象-关系 DBMS 技术代表相当新颖的事物。在软件开发中最大的挑战可能是快速响应最终用户,目前是指客户和消费者。以 Web 速度进行这些调整需要一种用于服务器软件的新方法:该方法将性能、可伸缩性以及可靠性与灵活性和对现实世界建模的改进能力组合在一起。对这种有组织的信息系统的支持正是对象-关系技术的用武之地。

关于作者

Paul Brown 是 IBM Chief Informix Technology Office 的“首席管道工”。Paul 与 Informix 首席技术官 Michael Stonebraker 博士合著了 Object-Relational DBMSs: Tracking the Next Great Wave。他是 Informix 体系结构评审委员会的成员,他在 Informix 用户组会议以及伙伴论坛上定期发表演说,他还发表了有关数据库主题的许多论文。可以通过 pbrown1@us.ibm.com 与他联系。

 

IBM 和 DB2 是 IBM 公司在美国和/或其它国家或地区的商标或注册商标。

Java 和所有基于 Java 的商标和徽标是 Sun Microsystems, Inc. 在美国和/或其它国家或地区的商标或注册商标。

其它公司、产品和服务名称可能是其它公司的商标或服务标记。

IBM 版权和商标信息

相关文章:

  • 2021-12-05
  • 2021-12-05
  • 2022-12-23
  • 2021-05-20
  • 2021-11-02
  • 2021-10-10
  • 2022-02-09
猜你喜欢
  • 2021-12-16
  • 2021-12-08
  • 2021-04-06
  • 2021-10-08
  • 2021-07-27
  • 2022-02-07
  • 2022-01-02
相关资源
相似解决方案