应用UML进行数据库建模
介绍
当需要为软件系统系统提供一种可靠,灵活而又高效的对象持久化方法时,当今的设计师和架构师们面临着众多的选择。从技术的层面上,这个选择往往介于完全面向对象,对象关系混合,完全关系化和建立在公开或专有文件格式上的常规解决方案之间(如:XML,OLE的结构化存储)。从提供者的层面上,Oracle, IBM, Microsoft, POET 和其它的公司提供了相似,但是彼此间往往不相容的解决方案。
本文仅论述这些选择中的一种,即在完全关系数据库上层面向对象的类模型进行分层。这并不表明它是唯一、最好而又简单的解决方案,但是从实用的角度看,它是最常用的一种类型,却也是最容易被用错的一种。
我们先快速浏览两个设计领域的模型,并试图把它们连接起来:第一,介绍用UML表达面向对象的类模型;第二,关系数据库模型。
对每一个领域我们只涉及影响到我们任务的主要功能。然后我们将关注从类模型到数据库模型映射的技术和问题,包括对象持久性,对象行为,对象和对象标识之间的关系。我们将总结对UML数据profile的回顾(Rational Software 推荐)。一些面向对象设计,UML和关系数据库建模的相似性也会被提及。
类模型是UML用来表达软件系统逻辑结构的主要工件。 它用来记录数据需求和模型领域内对象的行为。本文不讨论创建和详细描述该模型的技术,我们将假设已经存在一个设计好的类模型,它需要映射到关系数据库上。
类模型
类在UML中是一个基本的逻辑实体。它定义了一个结构单元的数据和行为。一个类是一个模板或运行时创建实例和对象的模型。当开发一个逻辑模型,如UML中的结构层次,我们将明确地把它们当作类来处理。当面对动态图时,如顺序图和协作图,我们也要处理类的实例和对象,以及它们运行时的内部动作。数据隐藏和封装原则是基于作用域效果。类有它的内部数据元素。访问这些数据元素需要通过类对外的行为或接口。遵循这个原则会生成更易于维护的代码。
行为
行为使用了类定义的操作,在类模型中被记录。操作是可以外部可见的(public),对子类可见的(protected)和隐藏的(private)。通过结合隐藏数据和公共访问接口,隐藏或保护数据的操作,类的设计人员可以建立极易维护的结构单元,这些结构单元是支持而不是阻碍变化的。
关系和特性
关联是两个类之间的一种关系。关系一侧的类知道和在某种程度上使用或操控另一侧的类。这种关联可以是功能上的(为我做某事)也可以是结构上的(是什么)。在本文中更多的是侧重结构上的关系。如:一个“Address”类可以关联一个“Person”类,将这种关系映射到数据空间需要多加注意。
聚合是关联的一种形式,表示一个类多个对象的集合在另一个类中。复合是一种更强的聚合形式,说明一个对象实际上由其它对象构成。对于关联关系来说,它意味着一个复杂的类属性,将该属性映射到关系模型时需要更详细的考虑。当一个类表示为生成许多对象实例的模板或模型时,对象需要在运行时有识别自己的方式,这样被关联对象可以对正确的对象实例施加作用。在编程语言中,如C++,对象指针可能会传递,并使所指对象可以访问一个独一无二的对象实例。通常尽管一个对象会被销毁,但是在需要时,又象上一次有效实例期间那样被重建。所以,这些对象需要一个存储机制来保留它们的内部状态和关联,并在需要时恢复所需状态。
继承给类模型提供一种方式,该方式提取通用行为到泛化的类中,使这个泛化类稍后可以做为在一般主题上诸多变异的原形。继承是一种管理重用和复杂性程度的方式。如我们将看到的,关系模型并没有与继承关系的直接对应项,这给数据模型建立者建立一个从对象模型到关系框架造成了困难。从一个运行时的对象到另外一个对象的导航是建立在完全引用的基础之上。一个对象有多种形式的连接(指针或唯一的对象标识),用这些连接可以定位和重建所需的对象。
关系模型
关系数据模型已经使用多年,提供的性能和灵活性一直行之有效。它本质上是基于集合的(set-based),并且用‘表’做为它的基本单元,表由一个或多个‘列’组成,每一个列包含一个数据元素。
表和列:一个关系表是一个或多个列的集合,每个列在表结构中有一个唯一名称,并且被定义成一个特定基本数据类型,如:数字、文本、二元数据。表定义是一个模板,表的“行”从这个模板中被创建,行可能做为一个表实例的实例。关系模型仅仅提供一个公共数据访问的模型。所有数据向外对任何一个过程开放,以便于被更新,查询和操控。信息隐蔽(information hiding)是未知的。
行为
与表相关联的行为通常是基于所应用实体的业务或逻辑规则。约束以多个形式应用到“列”,如:独特性需求、对应其它表和列的关系完整性约束,允许的值和数据类型。
触发器提供了关联到一个实体的许多附加行为。通常在数据被插入、删除和更新前后,强制执行数据的完整性检查。数据库存储过程提供了一种通过专有语言扩展来延伸数据库功能的方式,这些扩展通常用来构造功能性单元(脚本)。这些功能不会直接映射到这些实体,也不会与它们有逻辑关系。通过关系数据集的导航是基于“行”遍历和表连接实现。SQL是用来从表集选择“行”和放置实例的一种主要的语言。
关系和识别
表的主键为识别行提供一个特定值。这里有两种我们关注的主键:首先是意义键(meaning key),它由数据列构成,这些数据列在业务领域有意义。其次是一个抽象的唯一标识符,如计数器值,它没有商业意义,但是可以唯一地标识一个行。我们将先讨论抽象唯一标识符,然后再阐述意义键。一个表可以包含映射到另一个表主键的“列”。表间的关系定义了一个外键,说明了在这两个表之间的结构关系或关联。
小节
从以上的概述,我们可以看出对象模型是建立在离散实体基础上,这些实体既有状态(属性和数据),也有行为,一般仅通过类的公共接口来访问封装数据。关系模型同等地显露所有数据,有限支持利用触发器从行为到数据元素的关联。依靠使用唯一对象标识符,可以从一个对象移动到另一个对象,这使得我们可以在对象模型中导航,并建立对象关系(类似于网络数据模型)。在关系模型中,通过使用检索标准,SQL合并和过滤结果集,你可以查找找所需的行。标识符在对象模型中既可以是实时引用,也可以是持久的唯一标识符(称作OID)。在关系领域里,主键定义了数据集在整个数据空间中的唯一性。
对象模型中有丰富的关系集合:继承,聚合,关联,复合,依赖,以及其它。在关系模型中,可以仅使用外键来指明一种关系。我们已经对感兴趣的两个领域进行了介绍并比较了每一个领域的几个重要功能,然后将简单了解UML中关系数据模型的标注。
UML数据模型Profile(特性描述)
数据模型Profile是Enterprise Architect的UML扩展来支持关系数据库建模。它包括一些定制扩展,如:表,数据库图表,表键,触发器和约束。它是一种在UML中对关系数据库建模的技术。
表和列 表在UML数据Profile中是带《Table》构造型的类,它在右上角显示一个表的符号。数据库中的列用《Table》类的属性来建模。
例如:上面图型显示与客户表关联的属性。在此示例中,对象ID被定义为表的主键,还有两个列:“Name”和“Address”。注意上图例子中列的数据类型是按照原DBMS的数据类型定义的。
行为 到目前为止,我们仅定义了表的逻辑(静态)结构。此外,我们将描述与列关联的行为,包括:索引,键,触发器,过程等等。行为表示为带构造型的操作。
下图显示我们讨论的表,它有一个主键约束和索引,均被定义为带构造型的操作。
注意:“OID”列上的PK标签定义了逻辑主键,而构造型操作“?PK? idx_customer00”定义了与主键实现相关联的约束和行为(即主键的行为)。
对上例进行增加,我们现在可以定义附加行为,如:触发器,约束,存储过程。见下图:
这个例子描述了下列行为:
- 一个主键约束(PK);
- 一个外键约束(FK);
- 一个索引约束(Index);
- 一个触发器(Trigger);
- 一个唯一性约束(Unique);
- 一个存储过程(Proc)
- 一个有效性检查(Check).
使用上面提供的标注,我们可以在DBMS层次上,对复杂的数据结构和行为建模。另外,UML还提供表达逻辑实体间关系的标注。
关系
UML数据建模profile定义了两个表间任意一种依赖关系。它表示为一构造型的关联并包括一组主键和外键。数据profile仍然需要一种关系一直参与到父类和子类之间,父类定义一个主键,子类实现一个建立于全部或部分父类主键基础上的外键。这种关系将被区分为:“定义的”(如果该子类外键包含所有父类主键元素)和“非定义的”(如果只包括部分主键元素)。这个关系可以包括基数性约束,以及采用成对的相关主键与外键来建模,并命名为关联角色。下图描述了使用UML对这种关系的建模。
物理模型
UML也提供一些机制来表示数据库的整体物理结构,它的内容和部署位置。我们用构造型组件来表示一个物理数据库。见下图:
一个组件表示了一个离散,可部署的实体。在物理模型中,组件可以映射到一个物理硬件(UML的节点)。对于数据库内的关系模式,我们用带《Schema》构造型的包来表示。一个表可以放置到《Schema》中来建立它在数据库中的范围和位置。
从类模型到关系模型的映射
我们已经描述了所关注的两个领域和它们使用的标注,现在将我们的注意力转移到如何映射,及如何从一个领域转换到另一个领域。以下采用的策略和表达顺序是建议性的,而不是必须的。采用何种步骤和过程要根据个人的需要和环境而定。
1. 类的建模
首先我们假设从一个已经创建的类模型构建一个新的关系数据库模式。显然这是一个最容易的方向,这是因为模型一直在我们的控制之下,并且我们能根据类模型来优化这关系模型。在现实环境下,你可能需要在原有数据模型之上对类模型分层,这是更难的一种情况,具有挑战性。在这里,我们只关注第一种情况。至少类模型会记录元素的关联,继承和聚合关系。
2. 标识持久对象
建立类模型后,我们需要将类模型的元素分成需要持久性和不需要持久性两类。例如,如果我们采用“模型-视图-控制”的设计模式来设计我们的应用程序,那么只在模型部分的类将需要持久状态。
3. 假设每一个持久类映射到一个关系表
这是一个大的假设,但是它对大多数情况有用(现在先把继承的问题放一边)。在一个最简单模型中,一个来自逻辑模型的类映射到关系表的全部或一部分。这个映射的逻辑扩展是一个单独的对象(或类的实例)映射到表中单独的“行”。
4. 选择一个继承策略
继承可能是从面向对象模型转换到关系模型过程中最容易出现问题的关系和逻辑结构。关系领域本质上是平面的,每一个实体自身完成,而对象模型常常是一个具有深度的,设计完善的类层次结构。这样深度的类模型可以有多层的继承属性和行为,运行时生成最后全功能的对象:
- 每一个类层次结构有一个单独对应的表,这个表包含所有元素的继承属性-因此,这个表是该层次结构中每个类的联合。例如,人,父母,孩子和孙子可能形成一个单独的类层次结构,并且每个类中的元素都会出现在相同的关系表中;
- 类层次结构中的每一个类有一个对应的表,该表有仅能被该类访问的属性(包括继承属性)。例如,如果“孩子”仅仅从“人”继承而来,表将仅包含“人”和“孩子”的元素;
- 类层次结构中的每个类的自有属性对应一个表。例如,“孩子”将映射到一个仅带孩子属性的单个表
对每一种方法,我们有对应的案例。但是我们推荐第三种方法,因为它最简单,最容易维护和最不容易出错。第一种方法提供了运行时最佳的性能。第二种方法是第一种与第三种的折衷。第一种方法展开层次结构,在一个表里放置所有的属性,这样方便类层次结构中对类的更新和提取,但是不易于验证和维护。与“行”关联的业务规则是难以实现的,因为表中的每一行都可能被实例化为层次结构中的对象。“列”之间的依赖关系可能变得相当复杂。此外,对层次结构中任何一个类的更新将可能影响层次结构中其它每个类,如表中的列被添加,删除和修改。
第二种方法是一种折衷方案,提供了更好的封装和消除空列。可是,对父类的修改可能需要在所有子类的表中进行复制,更糟的是,有两个或多个子类的父类数据可能被冗余地存储在许多表中;如果父类的属性被修改,那么要花相当的时间去查找相关的子表,并更新受影响的行。
第三种方法精确地反映了对象模型,它将层次结构中的每一个类映射成一个独立的表。父类或子类的更新是在正确空间的局部范围内进行的。对实体的任何修改被严格限定于单个表内,因此表的维护也就相对简单。缺点是需要在运行时重构结构层次,来精确产生一个子类的状态。一个“Child”的对象可能需要一个“Person”的成员变量用于表示它的父辈。由于这两者都需要加载,两次调用数据库来初始化一个对象。随着类结构层次加深,初始化或更新一个单独对象的数据库调用次数也随之增加。
当你映射继承关系到关系模型时,理解上述要点是很重要的,这样你就选择最适合你的方案。
5. 为每一个类添加一个唯一的对象标识符
在关系和对象的领域里,需要唯一标识一个对象或实体。在对象模型中,非持久对象在运行时通常被直接引用或对象指针来标识。一旦对象被创建,我们可以用它运行时的标识符来引用它。但是,如果我们将对象写入存储空间,那么如何按需要得到完全相同的实例是一个问题。最便捷的方式是定义一个OID(对象标识符),它在关注的命名空间是被确保唯一的。根据需要,这可能发生在类,包或系统的层级上。
系统级别OID的一个例子可以是一个使用微软“guidgen”工具创建的GUID(全局唯一标识符),如{A1A68E8E-CD92-420b-BDA7-118F847B71EB}。类级别的OID可以用简单得数字实现(如:32位计数器)。如果一个对象具有对其它对象的引用,它可以采用使用它们的OID的方法。那么,在运行时有效地将引用的对象从存储区加载到模型中。关于上述OID值的重点是它没有超出定义它为标识符的简单含义。它们仅是逻辑指针而没有其它意义。在关系模型中,情况往往是不同的。
关系模型中标识正常地用一个主键实现。主键是一个表中的一组“列”,它们合起来可以唯一地标识一个行。例如:名称和地址可以唯一地标识一个“客户”。其它实体,如:“销售员”引用“客户”,它们要实现基于“客户”主键的外键。这个方法存在的问题是:将业务信息(如客户名和地址)嵌入标识符中对我们目标的影响。设想三个或四个表全部具有基于“客户”主键的外键,对于一个需要修改客户主键(如:增加一个用户类型)的系统修改,那么既要修改客户表,也要修改与外键相关的实体,这个工作是十分巨大的。
另一方面,如果一个OID被当作主键实现,并为其它表建立外键,那么修改范围仅限于主表,并且修改的影响也因此大大减小。实际上,一个基于业务数据的主键也许会被修改。如:一个客户可以修改地址和名字,既然这样,这个变化需要被正确的传递到所有其它相关的实体,更不用提改变部分主键信息的困难。
一个OID总是引用同一个实体,而不管其它信息怎么改变。在上面的例子中,客户可以修改名称和地址,与它相关联的表不需要被修改。当把对象模型映射到关系表时,实现完全OID的标识符比采用业务联系的主键更加便捷。用OID做为主键和外键的方法将为对象提供更好的加载和更新效率,将维护服务减到最少。实际上,一个与业务相联系的主键可以替换为:
- 一个唯一性约束或相关列的索引;
- 嵌入类行为的业务规则;
- 或将上述两种结合起来.
再次重申,是使用有意义的键还是使用OID取决于被开发系统的实际需要。
6. 映射属性到“列”
通常,我们将简单的类数据属性映射到关系表的列。例如,一个文本或数字字段可以分别表示一个人的名字和年龄,这种直接映射不会引起什么问题,只是简单地在数据库服务商的关系模型中选择适当的,与类属性相对应的数据类型。
对复杂属性(即属性为其它对象)使用下面详细的步骤来处理关联和聚合关系。
7. 映射关联到外键
更复杂的类属性(即它们代表其它类)通常建模为关联关系。一个关联是对象间的结构关系。例如,一个人可以居住在一个地址,当这个人被建模,他会有城市,街道,邮编的属性。在对象和关系领域里,我们倾向于把这个信息当作单独的实体来构造:“地址”。在对象领域里,地址代表一个独一无二的物理对象,并可能带有一个唯一OID。在关系领域,一个地址可以是地址表中的一行,该表的主键被用作为其它实体的外键。
在这两种模型中,趋向于移动地址信息到独立实体,这有助于避免冗余数据和提高可维护性。所以对于每一个类模型中的关联,要考虑创建一个从子类到父类表的外键。
8. 映射聚合和复合
聚合和复合关系类似于关联关系,并通过“主键-外键”对来映射成表。但是,需要提醒的是:普通聚合(弱聚合)对关系建模,如:一个人住在一个或多个地址,在此例中,超过一个以上的人可能住在相同地址,如果这个人不在这里住了,与他相连的地址仍然存在。这个例子例举了关系术语中称之为“多对多的关系”,并且通常实现为一个独立的表,该表包含了从一个表格的主键到另一个表格主键的映射。
弱聚合的第二个例子是:一个实体在那里使用或排除了另一个实体的所有权。例如:一个“人”的实体拥有一组股票,这标明这个人可能与一个“股票”表中的某些股票有关联,也可能没有关系。但是每个股票可以联系一个人,或不与任何人发生关系。如果这个人不在了,这个股票将变为“无主”的,或者被传递给下一个人。在这个关系模型中,可以通过每个股票有一个“所有者”列来实现,这个“所有者”列将存储一个人的标识符(OID)。
强聚合形式则有与之关联的完整约束。复合表明一个实体由部件组成,并且这些部件对整体有依赖关系。例如:一个人可以有很多证明文件,如护照,出生证明,驾照等。这个人的实体可以由这样的一组文件组成。如果这个人从系统里被删除,那么证明文件也要被删除,因为这些文件被映射到一个唯一个体。
如果我们暂时忽略OID,弱聚合可能使用中间表来实现(对多对多的情况)或者在一个聚合的类或表采用一个外键来实现(一对多的情况)。在多对多关系的情况下,如果父类被删除,中间表中的实体也要被删除。在一对多的情况下,如果父类被删除,外键输入(如“所有者”)必须被清除。
在复合的情况下,外键的使用是强制的,约束条件是父类删除后,部件也必须被删除。逻辑上,复合是有一种含义,部件主键形成了全部主键的一部分。例如:一个人的主键由他证明文件组成。尽管实际上是相当冗长的,但逻辑关系上为真。
9. 定义关系作用
对于每一个关联关系,可能会进一步指明关系每一个端点的角色信息。通常包含主键约束名和外键约束名。图6例示了这个概念,这在逻辑上定义两个类之间的关系。另外,可能会在作用名上标明附加约束(即{非空})和基数性约束(即0..n)。
10. 模型行为
我们现在来讨论另一个难题:是映射部分还是所有的类行为到数据库商提供的功能上,这些功能以触发器,存储过程,唯一性,数据约束和关系完整性的形式出现。一个非持久对象模型通常可以实现一种或多种语言(如Java和C++)需要的所有行为。每一个类将用公共的,保护的和私有的方式描述它被赋予的行为和职责。
不同数据库商的关系数据库通常包含不同形式的,基于SQL的可编程脚本语言,用来实现对数据的操作。最常用的例子是触发器和存储过程。当我们混合对象和关系模型,要做的决定往往是:是实现类模型中所有的业务逻辑,还是将部分的业务逻辑放在关系DBMS中更常用而有效率的触发器和存储过程中。从完全面向对象的角度看,答案是避免使用触发器和存储过程,并且将所有行为放到类中。这样会使行为局部化,从而提供了一个更清晰的设计,简化了维护,并且提供了在不同DBMS提供商之间更好的移植性。
在现实世界,存储过程和触发器的设计目标底线可以是每秒至少执行几百次或几千次之多。如果为了追求模型的清晰,移植性,可维护性和灵活性,那么就将所有的行为放在对象方法中。
如果性能是关注的焦点,可以考虑将部分行为交给更有效的DBMS编程语言。注意到将对象模型与存储过程以安全方式集成所花的额外时间,包括远程影响和调试的问题,可能要比简单部署更高性能的硬件更多。
如前面所述,UML数据profile提供下列扩展(构造型操作),可用来对DBMS行为建模:
- 主键约束(PK);
- 外键约束(FK);
- 索引约束(Index);
- 触发器(Trigger);
- 唯一性约束(Unique);
- 有效性检查(Check).
11. 产生物理模型
在UML中,物理模型描述了物体如何在现实世界里部署:硬件平台,网络连接,软件,操作系统,动态连接库和其它组件。你需要生成一个物理模型来完成这个周期:从一个初始的用例或领域模型,到类模型和数据模型,最后到部署模型。通常对这个模型,你要创建一个或多个节点,这些节点可以存放数据库,并安装DBMS组件。如果数据库被分成多于一个的DBMS实例,你可以分配表的包《Schema》给单个DBMS组件来指明数据位置。
总结
在UML中,物理模型描述了物体如何在现实世界里部署:硬件平台,网络连接,软件,操作系统,动态连接库和其它组件。你需要生成一个物理模型来完成这个周期:从一个初始的用例或领域模型,到类模型和数据模型,最后到部署模型。通常对这个模型,你要创建一个或多个节点,这些节点可以存放数据库,并安装DBMS组件。如果数据库被分成多于一个的DBMS实例,你可以分配表的包《Schema》给单个DBMS组件来指明数据位置。