UML软件工程组织

业务对象到关系数据库映射的若干模式
 原文:Connecting Business Objects to Relational Databases

译者:Happy liu

 

摘要

这些模式描述如何把业务对象映射到非面向对象的数据库中。面向对象和非面向对象这两种技术存在着阻抗不匹配impedance mismatch,因为对象由数据和行为组成,而一个关系型数据库则是由表和它们之间的关系组成的。虽然不可能完全消除这个阻抗不匹配,你可以遵循适当的模式使之最小化。适当的模式可以向开发人员隐藏持久化细节,而让他们专注于理解域问题而不是如何将对象持久化。


简介

使用关系数据库的面向对象系统开发人员通常要花费大量的时间来将对象持久化,这是因为在两种技术间存在一个基本的阻抗不匹配。对象由数据和行为组成,通常可以继承,而关系数据库包括表、关系和基本的谓词计算函数,这个函数用以返回想要的值。

为避免对象和关系之间的阻抗不匹配,一种方法是使用一个面向对象的数据。然而,系统通常需要将对象存入一个关系型数据库,有的因为一个系统需要关系型理论或关系型数据库的成熟性,有的因为公司策略就是使用关系型数据库而非面向对象数据库。无论是什么原因,一个将对象存入关系型数据库的系统需要提供一个减少这个阻抗不匹配的设计。

本文只描述了将对象映射到关系上的部分模式语言,但是它描述了我们认为在其他地方没有描述充分的模式。全部模式的概要可以参见[Keller 98-2],其中阐述得较好的模式是关于关系型数据库设计和优化的[Brown 96][Keller 97-1,97-2,98-1]Serializer模式[Riehle et al. 1998]描述了如何串行化对象,让它们可以向不同后端存储和获取,例如文本文件、关系数据库和RPC缓冲。

我们曾使用或研究过若干持久对象系统(GemStone[GemStone 96], TopLink[TopLink 97-1,07-2]ObjectLens[OS 95])。另外,我们用VisaulAge for SmalltalkIllinois Department of Public Health(IDPH)实现了一个简单得持久化框架,这里介绍的模式存在于所有这些系统中。商业系统在这些模式上的使用通常比我们的框架更加彻底,我们曾更想购买一个持久化框架,但是我们的预算无法负担它们。我们这些需要使用持久框架的应用系统很简单,涉及到几十个数据表,每个应用管理一个病人的病历信息,所有应用共享病人统计信息,例如病人姓名、地址和医院、医生的信息,而每个应用各自负责某个领域,例如病人的免疫、血液检查等。虽然一个应用能够管理一个病人的大量信息,在某个时刻,它将只检查一个病人。本文的示例将向您展示在为IDPH开发的应用中,如何协同使用这些模式,解决持久化NameAddress对象的问题。

这些模式串在一起,手拉手地工作,来解决上面提到的阻抗不匹配问题。一个持久层将开发人员和实现持久化的细节隔离开,并保护开发人员不为变更所困。持久层是构建一个层的特例,保护您远离应用程序和数据库的变更。实现持久层的方式之一是通过一个PersistentObject,另一个方法是通过一个中间人(Broker)[BMRSS 96]

向数据库读取和写入需要基本的创建、读取、更新和删除操作,虽然每个对象能够有它们自己的访问数据库接口,但是如果您的系统向持久层提供一组共通操作,那么所有的对象都可以使用,这样的系统就会更易使用和维护。不管您采取哪种实现持久层的方式,都需要支持CRUD(创建、读取、更新和删除)操作。

CRUD操作最终会使用SQL代码访问数据库,有某种SQL代码描述来构建实际的数据库SQL调用是很重要的。

当从数据库取出值或者将值存回数据库,系统必须进行属性映射来映射数据库字段值和存储在对象属性中的值。阻抗不匹配的一部分就是关系型和对象系统有着不同类型的数据,映射对象和数据库的值需要转换两种技术中值的类型。

当对象属性发生改变后,将它们存入数据库是很重要的,因此,任何持久对象系统都应该使用某种变更管理器来跟踪哪些对象发生了变化,使得系统能够跟踪到哪些对象已经被改变,以确保根据需要保存。变更管理器同时也有助于减少数据库访问,因为它只为改变过的对象创建事务并保存。

因为在面向对象系统中每个对象都是唯一的,通过一个OID管理器为每个新对象创建一个唯一标识就很重要。同时,支持事务也是非常重要的,它确保改变一个对象是一个原子操作,可以通过一个事务管理器回滚该操作。任何访问一个RDBMS的系统将通过某种联接管理器提供对目标数据库的联接。通过一个表管理器处理数据库表名、字段名也是非常有益的。

1中的模式目录概括了本文中讨论的模式,它列出了每个模式的名称,以及它所解决问题的简要描述。

 

模式名称

描述

持久层

提供一个层,将您的对象映射到关系数据库或其他数据库上

CRUD

所有持久对象至少需要的创建、读取、更新和删除操作。

SQL代码描述

定义实际的SQL代码,从关系数据库和其他数据库中取得值,被对象所用,反之亦然。它被用来生成执行CRUD操作的SQL代码。

属性映射方法

映射数据库值和对象属性值,这个模式也处理复杂对象的映射,根据数据库表的一条数据行产生对象。

类型转换

属性映射方法一起使用,在数据库类型和对象类型之间转换,确保数据完整性。

变更管理器

为维护数据完整性,跟踪对象值的变化情况,由它决定是否需要写入到数据库中。

OID管理器

在插入时为对象产生唯一的对象ID

事务管理器

当保存对象时提供事务处理机制。

联接管理器

得到并维护数据库联接。

表管理器

管理一个对象和数据库表、字段的映射

1 模式目录


持久层

别名:

关系数据库访问层

 

动机:

如果您构建一个大型的面向对象业务系统,而将对象保存到关系型数据库中,您可能要花很多时间去处理如何使对象持久化。如果您不够仔细,开发系统的每个程序员都不得不了解SQL代码以及访问库的代码,从而被数据库所约束。将您的系统从Microsoft Access转到DB2上会有大量的工作,乃至为一个对象添加很多变量。所以您需要将您的领域知识从对象是如何存储的知识中分离开,保护开发人员不会为这些变化所困。

 

问题:

如何将对象保存到一个非面向对象的存储机制中?例如关系型数据库,而开发人员不必知道实际的实现方式。

 

特定约束:

Ÿ           对熟悉数据库的开发人员来说,写SQL语句很容易;

Ÿ           设计一个优秀的持久化机制需要花费时间,但是它不直接给用户提供什么功能;

Ÿ           数据库访问代价不菲,通常需要优化;

Ÿ           开发人员应该可以不必担心如何在数据库中存取而专注于解决应用系统的业务域问题;

Ÿ           有一个使用模板方法的共通接口,使代码更易重用;

Ÿ           使用一个单一的接口将强制所有的类拥有最低程度的共同性;

Ÿ           在应用系统的生命周期中,持久存储类型有可能会改变;

Ÿ           在应用系统的生命周期中,业务模型有可能经常会变;

 

解决方案:

提供一个持久层,可以从一个数据存储源中生成对象,并可以把数据保存到数据存储源中去。这一层向开发人员隐藏了对象存储的细节,这实际上是构建一个层(Layer)[BMRSS96]的特殊情况,使您自己免于变化之苦。所有持久对象都使用持久层的标准接口,如果数据存储机制改变了,只有持久层需要改变。例如,公司主管在项目开始使用Oracle,到项目中期又转到DB2上。

系统需要知道如何存储和装载每个对象,有时候一个对象存储在多个媒介上的多个数据库中。一个对象作为另一个更复杂对象的部分,需要跟踪它是哪个对象的一部分,这叫做所有者对象。这个所有者对象的概念使编写复杂查询变的很容易,因此,持久层为每个对象和它的父对象提供唯一标识是很重要的,这个唯一标识符和父标识符在实现Proxy模式时非常有用,可以作为部件对象的占位符。

总结一下模型名称的使用,持久层提供必要的方法,通过构造SQL代码提供CRUD操作,提供属性映射方法,为对象数据值进行类型转化,访问表管理器,提供对事务管理器的访问,通过联接管理器联接到数据库。同时持久层也有助于提供适当的变更管理,并和OID管理器协作提供唯一的对象标识。

有很多方法可以实现一个持久层,这儿列举一二。

1. 使用一个对象层[Keller 98]。每个域对象从一个抽象的PersisentObject类继承,知道如何执行必要的CRUD操作。本文示例使用的就是这种方式,它的主要好处是易于实现。虽然它在每个域类中都要写一些数据库相关的代码,不过这些代码是分开的,易于查找和修改,它可以在必要时进行优化,尽管一个过于优化的系统难以理解。

2. 使用一个中间人,它可以从数据库读取域对象或将对象写入数据库,中间人必须知道每个域对象的格式,生成SQL语句去读写。这种方式将数据库代码和域对象类分离开来,是一种最具伸缩性的解决方案,不过需要很多基础部件。

3. 用一组数据对象组成一个域对象,这些数据对象和数据库表具有一对一关系。这样,一旦一个域对象改变了,改变相应的数据对象,并且在域对象保存时,他们也将被保存。例如,一个Patient的值可以映射到NameAddress数据表,Patient对象将拥有映射到NameAddress的数据对象。VisualWorks中,ObjectShareObjectLens就是采用这种方式构造数据库对象。持久层通过这些数据对象管理起来,它很容易实现并易于理解,尽管有些慢,并且开发人员必须要维护数据库表的一一映射关系。

 

注:主要的决策依据应该看对灵活性、伸缩性和可维护性的要求。

 

实现举例:

现在很多关于正确描述构建中间人[BMRSS96]的细节工作已经完成,而且我们拥有更多实现层对象的经验,因此,我们的例子将主要关注于这个模式的实现。其他在实现中间人中描述的模式在我们讨论中也将简短提到。我们所有的示例代码都将描述基于PersistentObject的实现。

1是一个UML类图,表示一个持久层将域对象映射到关系数据库的实现。请注意在这个例子中,需要被持久化的域对象是PersistentObject的子类,PersistentObject持久层提供接口。PersistentObject表管理器交互,可以为SQL代码提供物理表名,在SQL代码生成时,PersistentObject联接管理器交互以提供必要的数据库联接。如果需要,当需要一个新的唯一标识时,向OID管理器请求。这样,PersistentObject作为一个中间的集线器,为域对象提供需要而它本身没有的任何信息。PersistentObject持久层提供标准的接口,通过和其他模式一起合作,一旦SQL代码准备好了,SQL语句将被数据库部件触发,在IBM VisualAge for Smalltalk中,这些就是AbtDBM*应用系统。

PersistentObject的属性如下:

Ÿ           objectIdentifier 对象唯一的标识符,可以是数据库键值。

Ÿ           isChanged 标志对象是否被修改过,告诉持久层这个对象是否要写入数据库中。

Ÿ           isPersisted 标志对象是否曾写入到数据库中。

Ÿ           owningObject 标识父对象,在数据库中作为一个外键使用。注意这个外键在本对象而不是父对象中。

PersistentObject的公共方法如下:

Ÿ           save 将对象数据写入到数据库中,它将更新或插入行;

Ÿ           delete 从数据库中删除一个对象的数据;

Ÿ           load 从数据库中返回一个类的单一实例及它的数据;

Ÿ           loadAll 从数据库返回一个类的实例集合,包含所有数据,这对为选择列表返回数据非常有用;

Ÿ           loadAllLike 从数据库返回一个类的实例集合,包含部分数据;

 

1 持久类类图

 

数据库记录可以以三种方式读取:

Ÿ           读取一行(PersistentObject>>load:)

Ÿ           读取所有记录(PersistentObject>>loadAll);

Ÿ           读取所有符合条件的记录(PersistentObject>>loadAllLike

指定一个特定条件,创建一个对象的新实例并为它装载对应的属性集,这个功能可以通过PersistentObject>>load:PersistentObject>>loadAllLike:方法实现。而当您想产生一个选择列表或是下拉列表时,PersistentObject>>loadAll方法是非常有用的。

下面的示例代码描述了上面所说的PersistentObject,它们是公共的接口方法,支持事务管理(后文详述)。read:saveAsTransaction方法将在CRUD模式中详述。

 

Protocol for Public Interface PersistentObject (instance)

load

       “得到匹配它自己的PersistentObject子类的单一实例”

    | oc |

    oc := self loadAllLike.

    ^oc isEmpty ifTrue: [nil] ifFalse: [oc first]

loadAllLike

       “得到匹配它自己的PersistentObject子类的实例集合,selectionClause方法为PersistentObjectread方法准备Where子句”

    ^self calss read: ( self selectionClause )

save

       “保存他自己到数据库中,包含在一个事务当中。”

    self class beginTrasacction.

    self saveAsTransaction.

    self class endTransaction.

delete

       “从数据库中删除他自己,包含在一个事务当中。”

    self class beginTrasaction.

    self deleteAsTransaction.

    self class endTransaction.

 

Protocol for Public Interface PersistentObject (class)

loadAll

       “从数据库返回我的所有实例”

    ^self read: nil.

 

下面是Name类的示例代码,NameAddress,因此它将有一个部件,需要被存储,这个方法被任何需要被存储而拥有持久化部件的域对象重载。

 

Protocol for Private Interface Name (instance)

saveComponentIfDirty

       “验证address对象的存在,并验证proxy模式没有占据这个位置,address所有者对象被设为当前对象,并且address对象的保存也是当前事务中的一部分。”

    (self address isNil or: [self address isKindOf:

                                PPLAbstractProxy])

       ifTrue: [^nil].

    self address owningObject: self objectIdentitier.

    self address saveAsTransaction

 

结论:

Ÿ           把应用开发人员从对象存储细节中隔离开来的另一个好处是易于实现域对象,因此,域模型变更的工作就变少了。总之通过封装对象持久机制的功能,可以有效地对开发人员隐藏对象存储的细节。

Ÿ           可以改变数据库技术,而不影响您的应用程序代码。

Ÿ           使改变对象存储到数据库的方式变的很容易,因为我们已将需要改变的地方隔离起来了。

Ÿ           用户只需要调用同样的接口去持久化对象,开发人员无需检测一条记录是否已经存在于数据库中。

Ÿ           SQL代码实现起来很简单的事情,用持久层可能使它变的复杂而且有时候难以操作。

Ÿ           持久层的优化很困难。程序员应实现对不同方式做评测来决定哪一个更适合他的实现。

 

相关或交互的模式:

Ÿ           关系数据库访问层是一个很相似的模式,它描述了一个需要持久化的对象交互的层。

Ÿ           分层架构[Shaw 96]描述了必要的模式,将体系架构分层,以便在开发中隔离变更。

Ÿ           信息系统的分层架构讨论了开发分层系统的实现细节。

Ÿ           [BMRSS 96]描述了分层系统架构和设计时,需要考虑的细节。

 

已知应用:

Ÿ           GemStone OODBMS使用持久层隐藏一个值是一个持久对象的事实。在实际需要时使用代理为持久层获取值,在这个例子中,存储系统不是关系型数据库。

Ÿ           Caterpillar/NCSA金融模型框架[Yoder 97]使用持久层,所有的值都通过一个Query对象存储。这个例子中,应用程序在数据库中不存储任意域对象,只是获取事务。然而,持久层仍隐藏了数据库和关系型数据库技术的细节。

Ÿ           ObjectShare VisualWorks Smalltalk ObjectLens[OS 95]使用持久层映射数据对象和数据库表。

Ÿ           VisualAge for Smalltalk也使用持久层和它们的AbtDbm*应用。VisualAge提供图形化联接的GUI构建器,形成一个持久机制。

Ÿ           Illinois Department of Public Health’s TOTSNewBorn Screening项目使用一个和本节例子非常相似的实现方法。

Ÿ           TopLinkMicroDocSparkyObject Extencer[MicroDoc 98, Sparky 98, OE 98]都提供一个持久层来将对象映射到关系型数据库中。

Ÿ           PLoP登记系统实现了一个持久层,将Java对象存到PostGress数据库中[JOE PUT THE REF HERE]


CRUD

 

别名:

       创建、读取、更新和删除

       基本持久操作

 

动机:

试想你有一个Patient类,有NameAddress部件,当你读取一个Patient,必须同时读取NameAddress。写入一个Patient到数据库中将有可能写入一个NameAddress对象。他们是否有同样的接口去读取和写入呢?也许有些对象需要不同的接口?我们能否给出完全同样的接口,如果可以,是什么?

任何被持久化的对象都要对数据库进行读取和写入,对新创建的对象,它的值也会被持久化,另外,对象也可以从持久存储中删除,因此,如果一个对象需要持久化,至少要提供最小的操作集合,他们是创建、读取、更新、删除。

 

问题:

一个持久对象最小的操作集合是什么?

 

特定约束:

Ÿ           保存在数据库中所有的对象需要一个装载自己、保存自己的机制;

Ÿ           在一个地方放置读取和写入的代码有助于对象的演进和维护;

Ÿ           如果类只实现同样且小的接口,可以很容易将他们组成嵌套的类。

 

解决方案:

为持久对象提供基本的CRUD(创建、读取、更新和删除)操作。其他需要的操作如loadAllLike:loadAll。重要的是要提供足够多的信息能够从数据库实例化对象,并保存新建的或改变了的对象。

如果所有域对象都有一个共同的PersistentObject超类,那么这个超类可以定义CRUD操作,而所有的域对象能够从它继承,如有必要,子类可以重载他们以提高性能。

如果持久层是使用中间人实现的,那么CRUD操作也由中间人实现,不论什么情况,持久层必须生成SQL代码来读取和写入域对象。这样,每个域对象必须能够获得必要SQL代码的描述,来访问CRUD操作的数据库。CRUDSQL代码描述紧密合作,确保这些操作能有效持久化域对象。

 

示例实现:

前面阐述的PersistentObject提供了标准接口,一组基本的操作来映射对象到数据库,保存、装载等。这些方法从PersistentObject继承,访问CRUD操作。有些CRUD方法需要在域对象中重写。AbtDBM*数据库部件提供了executeSql: 方法,让数据库执行SQL语句并返回值。updateRowSqlinsertRowSql将在下面SQL代码描述模式中详述。

 

Protocol for CRUD PersistentObject (class)

这个方法指定一个WHERE子句作为中介,并返回一组和WHERE条件相匹配的对象集合。

read: aSearchString

       “从数据库返回一个对象实例集合。”

    | aCollection |

    aCollection := OrderedCollection new.

    (self resultSet : aSearchString)

       do: [:aRow | aCollection add:self new initialize: aRow].

    ^aCollection

 

Protocol for Persistence Layer PersistentObject instance

这些方法对数据库保存或删除对象,这些方法要基于对象的值判断执行什么SQL语句(insertupdatedelete),一旦决定,SQL语句将在数据中执行。

saveAsTransaction

       “保存自己到数据库中。”

    self isPersisted ifTrue: [self update] isFalse: [self create].

    self makeClean

update

       “更新聚合类,然后在数据库中更新他自己”

    self saveComponentIfDirty.

    self basicUpdate

create

       “插入聚合类,然后在数据库中插入自己”

    self saveComponentIfDirty.

    self basicCreate

basicCreate

       “在数据库中触发插入SQL语句”

    self class executeSql: self insertRowSql.

    isPersisted := true

basicUpdate

       “在数据库中触发更新SQL语句”

    (self isKindOf: AbstractProxy) ifTrue: [^nil].

    isChanged ifTrue: [self class executeSql: self updateRowSql]