类型转换
别名:
数据转换
类型翻译
动机:
数据库值类型并不总是和对象类型直接对应,例如,一个布尔值也许在数据库存成T或者F,在Patient例子中,性别可以是一个属性,以一个名为Sex的类存储,男性实例的某些行为,而女性实例有另外不同的行为,在数据库中也许他们的值是M和F,当从数据库读取这个值,M需要转换成一个Sex类的男性实例,F需要转换成Sex类的女性实例。类型转换允许对象值和数据库值之间的转换。
问题:
如何将一个没有对应数据库类型的对象映射到一个数据库类型,并反之亦然?
特定约束:
? 数据库中的值也许不能映射到指定的对象类型上;
? 对象类型和数据库类型之间存在着阻抗不匹配;
? 应用程序中的值可以被其他应用程序在数据库中编辑和存储;
? 对象属性和数据库字段值能够都存成字符串和数字,这样就减小了阻抗不匹配;
解决方案:
通过一个类型转换器对象把所有值转换成各自的类型,这个对象知道如何处理空值以及其他对象值和数据库值之间的映射。当一个对象从大型多应用数据库被持久化,数据格式会多种多样,这个模式确保了从数据库中获取的数据能适合于对象。
确保从数据库中读取的数据能够为对象工作是很重要的,同时从对象写入数据库的数据遵从数据库规则和维护数据的一致性也是非常重要的。
每个对象属性通过适当的类型转换器传递,为特有的应用系统和DBM应用必要的数据规则,在域级别应用的数据规则比在接口级别中应用的要高效得多,这通常是由于所有的域对象都使用一组共同的类型转换。
想象一下有些旧的数据库没有空值(NULL)的概念,它们为“无数据”的情况实际上存储一个空字符串,当读出这个值,返回一个对应用程序表示“空白数据”的空字符串,他依赖于数据库和该应用程序的规则定义。
这个类型转换可以在映射代码中直接完成,数据类型将被转换成适当的对象类型,或者反之亦然,通常,一组共通的转换可以抽象出来。例如,一个Boolean对象可以映射数据库中的T或F,Timestamps能够映射到String上,一个NULL可以映射到一个空字符串。当你有了这些共通的转换,就可以调用适当的例程为转换预处理类型,这些转换例程形成了策略(Strategy) [GHJV95]的一部分。
根据你的需要,实现是多种多样的,下面的例子中,将所有的转换方法放置在一个对象中,这使方法都存在于一处,如果有必要,允许动态地切换转换对象。这样,也能够用一个策略,为不同数据库应用不同转换算法,如果余下的应用中,转换器不再需要,将是一个更清洁的方式。
另一个选择将扩充每个受影响或被使用的基类,每个对象将知道如何将他们转换成数据库必须的格式,假设你使用了多个数据库,那么每个方法都需要适应这个格式之间的差异。
还有一个方式,将你所有的转换例程放在PersistentObject中,如果你需要映射到一个新的数据库或是转换改变了,就隔离出不得不修改代码的地方。这个方法存在于任何从PersistentObject继承而来的对象,类似前面一种选择,当你有多个数据库时,情形是相似的。
所有提到的这些方法都可以独立于所选的持久机制而工作。
实例实现:
当你从一个数据库中字典结构的一行值产生属性时,可以简单地使用:
attribute := (aRow at: key).
这可以完成任务,然而,如果数据库值不能保证符合对象所需,那么你使用该属性的代码将失败,当你应用了类型转换,如:
attribute := self typeConverter convertToUpperString:
(aRow at: key).
这个属性值将是应用程序所需的,这会先得到负责转换的类,然后把数据库值传递到一个方法中,确保产生一个大写字符串的属性值。
把准备对象的属性值存储到一个数据库的情形是相似的,你可以简单地将属性值放入一个字符流。
nextPutAll: (attribute) printString.
这也可以完成任务,然而,如果碰巧一个对象不知道printString,那么这个代码将会失败。should the database … a conditional statement. 当你应用类型转换,如:
nextPutAll: (self typeConverter prepForSql:
(attribtute asUppercase)).
属性将被转换成适当的格式,如上所述,负责转换的类被得到并且属性将被转换。
类型转换方法通过PersistentObject访问,既可以准备存入数据库的值,也可以将数据库返回的行映射成对象属性。initialize:和insertRowSql:方法(在每个域对象中)展示了类型转换的例子。
Protocol for Type Conversion PersistentObject (instance)
这个方法决定哪一个类负责为表类型和对象类型转换类型,这个方法可以延伸为,在运行期决定使用哪个数据库或使用哪个类来转换类型。
typeConverter
“返回负责类型转换的类”
^TypeConverter
Protocol for Type Conversion TypeConverter (class)
这些方法为PersistentObject提供一致的格式化的数据值,当从对象向数据库转换时(TypeConverter>>prepForSql:),根据数据库需要来决定和格式化类型;当从数据库向对象转换时,数据库的类型也许不是对象/应用程序所要的,在属性映射方法中,每个属性经过类型转换以确保数据值是正确的。在有些情况下,当数据库不包含数据时,提供一个缺省值。当若干个不同实现的应用程序使用同一个数据库时,数据规则在数据库级别并不需要强制遵从。
convertToBooleanFalse: aString
“从一个字符串返回一个布尔值,非值为默认值。”
^’t’ = aString asString trimBlanks asLowercase
convertToString: aString
“从一个数据库字符串或字符返回一个字符串,缺省值是一个新字符串。”
^aString asString trimBlanks
convertToNumber: aNumber
“从一个数据库数字返回一个数字,缺省值是0。”
^aNumber isNumber ifTrue: [aNumber asInteger] ifFalse: [0]
这个方法把一个对象转换成正确的数据库格式,放在一个字符流中,对象被测定为什么类型后返回适当的格式,形成SQL代码放置在字符流中。这为持久层数据类型提供了一个共通的格式。本例中缺省的日期格式假设为(本地当前时间:’%m%d%Y’)。如果你不想使用一个完全日期格式,日期的格式应该根据你的数据库修改。注:这些格式类型被IBM DB2 UDB V5.0支持。
prepForSql: anObject
“以字符流的形式返回具有适当格式的对象。”
anObject isNil ifTrue: [^’NULL’].
anObject isString
ifTrue:
[anObject isEmpty
ifTrue: [^’NULL’]
ifFalse: [^anObject trimBlanks printString]].
anObject isNumber ifTrue: [^anObject printString].
anObject abtCanBeDate ifTrue:
[^anObject printString printString].
anObject abtCanBeBoolean ifTrue:
[anObject ifTrue: [^’T’ printString]
ifFalse: [^’F’ printString]].
anObject abtCanBeTime
ifTrue: [^self databaseConnection databaseMgr
SQLStringForTime: anObject].
(anObject isKindOf: PPLPersistentObject)
ifTrue: [anObject objectIdentifier isNil
ifTrue: [^’NULL’]
ifFalse: [^anObject objectIdentifier printString]]
结论:
? 能帮助确保数据一致性;
? 对象类型可以有很多种,和数据库类型无关;
? 可以替从数据库读出的空值赋缺省值;
? 阻止“不理解未定义对象(Undefined object does not understand)”的错误;
? 为应用程序提供增强的RMA(可靠性、可维护性和可访问性);
? 转换类型需要花费时间,特别是从数据库读取大量的值时;
相关或交互模式:
? 策略[GHJV 95]可以用来实现这个模式;
? 当构造SQL代码时,属性映射方法调用类型转换;
? SQL代码描述可能嵌入类型转换的调用。
已知应用:
? Illinois Department of Public Health TOTS和NewBorn Screening 项目;
? ObjectShare的VisualWorks Smalltalk[OS 95]使用类型转换来在数据库类型和对象类型之间转换。VisualAge for Smalltalk也在他们的AbtDbm*应用程序中使用类型转换。
? GemStone GemConnect在数据库类型和对象类型之间转换;
? TopLink也提供类型转换。
变更管理
别名:
HasChanged
IsDirty
Laundry List
对象管理器
动机:
通常使用一个病人管理系统的方法是为病人调出他的记录和为最近拜访的病人增加一条记录。但是有时候病人的地址已改变了,如果发生了实际改变,系统应该只写回病人的地址。
实现这个情况的方式之一是提供一个单独的改变地址的画面,一个单独的更新按钮用来将地址写回数据库,但这很笨拙并需要维护大量代码;更好的方法是,让病人管理系统的用户在必要时编辑地址,并让系统只写回地址对象的变化和其他用户决定要写回的值。
总的来说,一个持久层应跟踪所有PersistentObject的变更状态,并应确保他们都要写回数据库中。
问题:
如何判断一个对象改变了,且需要保存入数据库?防止对数据库不必要的访问,确保用户知道什么时候一些值被改变了并在退出程序前没有被保存是很重要的。
特定约束:
? 大多数对象读出来以后从没修改过;
? 保存没有改变的对象是浪费时间;
? 开发人员经常忘记保存一个修改过的对象,而用户更糟;
? 如果对象的每一个修改都写入数据库的,那么,为了取消用户的请求,就需要另一个写入或回滚操作;
? 复杂对象也许只有一个部件修改了,那无需将它所有的值都写入数据库;
? 将没有被修改的对象写入数据库,将难以稽核到底是谁最后修改了这个对象;
解决方案:
设置一个变更管理器,来跟踪任何PersistentObject修改了某个持久属性,无论何时,请求保存对象都需要这个变更管理器。
实现它的方式之一是从PersistentObject继承一个类,它有一个脏位,一旦一个映射到数据库中的属性值发生改变就被置位。这个脏位通常是一个布尔值实例变量,表明一个对象是否改变。当这个布尔值设定了,一个保存操作调用时,PersistentObject将保存新值到数据库中,否则,PersistentObject将忽略写入数据库。
洗衣列表(laundry list)也是一个用来保存数据的模式,它通过将变更存储在一个洗衣篮中工作,然后你能够控制对它们做什么,另外它还跟踪哪些属性被改变了,从而只保存脏属性。这样,如果一个应用程序改变了一个表的若干字段,而另一个应用改变了其他字段,你能确保只更新你所改变的字段,他也能够有助于应用程序并发的修改数据库表。
根据类体系的不同,实现方案有很多种。一种方案是修改属性设置(setter)方法,可以在对象值修改时设定一个标志,也可以把这个对象加入到已变更对象的洗衣列表,这非常简单却很有效。另一个方案可以使用类似于方法包装(method wrapper)[Brant, Foote, Johnson, &Roberts 1998],在属性设置方法中做同样的事情。还有一种方案,初始化持久属性成为一种依赖性机制,一旦一个属性值被修改了,可以设置脏位也可将对象加入洗衣列表,并删除依赖性;一脏永脏。另外一种实现方法是使用元数据来描述所有的持久属性,无论何时改变了对象的状态,都能够使用元数据来决定对象是否变脏了,方法包装能够使用这个元数据提供类似的服务。
数据访问一般非常昂贵,应该节约使用,通过标志一个对象是否需要写入数据库将显著提供性能。除了性能的考虑,也有助用户界面在退出前提示用户保存,这个功能使用户更容易接受你的应用系统,这能让他们知道自己忘记保存了已变更信息,而系统会提示他们保存。用户将形成一个结论,如果他们不得不重复输入同样的数据,这个系统便很快没有价值了。
变更管理器的另一个特性是提供对象的初始状态或改变状态的记忆功能,这可以通过Memento[GHJV95]来实现,如果你系统的用户需要回到初始状态,变更管理器能够保留初始值,通过调用一个撤销操作完成。同时,你也还可以为你的系统提供多步撤销操作。
如果你有一个对象,它的某个属性是其他对象的一个有序集合,(在本例中,Name的属性地址可以是Address类的OrderedCollection),当你删除一个Address的实例,数据库如何得到删除数据库行的消息?一种方法是将该实例的键值设为nil,然后,域对象提供一个isValid方法,判定删除特定行,这个方法可以实现,但是应用系统程序员不得不用特定的代码处理所有nil实例。一个更好的方式是使用一个删除管理器(Deletion Manager),当用户按下删除按钮(或其他机制),应用系统程序员把删除的实例放置其中,因为每个对象知道如何删除它自己,该实例会从集合中和数据库中被删除。删除管理器是一个Singleton[GHJV95]对象,它保留被删除的实例,直到用户发出保存或取消操作。
实例实现:
本例将展示一个存取器(accessor)方法如何为Name类的首名属性在修改值时设置脏位,这个访问者方法在VisualAge中缺省生成了附加的makeDirty调用,它将将继承的isChanged属性设置为真值。
first: aString
“保存首名值。”
self makeDirty
first := aString
self signalEvent: #first
with: aString
Protocol for Change Manager PersistentObject (instance)
这些方法为持久层提供改变脏标志的功能,这避免了持久层不得不向数据库写入没有改变的数据,同时也向GUI程序员提供一个测试对象的方法,以便向用户提供是否保存数据的提示信息。
makeDirty
“表明一个对象需要保存入数据库,如前面的例子中,这个方法可以在setter方法中调用。”
isChanged := true.
makeClean
“表明一个对象不需要保存入数据库或者对象没有被改变过。”
isChanged := false.
结论:
? 用户会更乐意接受这个应用系统;
? 不写入没有被改变的数据到数据库,可以保证数据库有更好的性能;
? 当在数据库间融合数据,可以设定该标志,这样这个记录在必要时将插入新的数据库中;
? 相关或交互的模式:
相关或交互模式:
? 状态(State)[GHJV 95]是一个使用布尔值标志的替代方法;
? Memento可以被变更管理器用来支持撤销操作;
? 洗衣列表能够跟踪所有改变了的对象;
? 可以使用删除管理器辅助删除复杂对象的一部分;
? 对象管理器[Keller 98-2]是一个非常相似的模式。
已知应用:
? 操作系统为虚拟内存使用脏位;
? 大多数DBMS在高速缓存中使用脏位[Keller 98-1];
? 高速缓存一般都使用脏位;
? Illinois Department of Public Health TOTS和NewBorn Screening项目;
? 在GemStone OODBMS中,使用一个变更管理器来跟踪一个对象何时被改变了,因此可以知道一个对象何时需要保存到服务器。
? ObjectShare的VisualWorks Smalltalk[OS 95]使用变更管理器指明一个对象值是否被改变过。VisualAge Smalltalk也在他们的AbtDbm*应用系统中使用变更管理器。VisualAge为GUI构建者提供图形化联接,以提供一个持久机制。
OID管理器
别名:
唯一键值生成器
动机:
只要一个对象被持久化,对象的唯一性是很重要的。在一个面向对象系统中,所有对象都是唯一的,所以给定每个对象一个唯一的标识是非常重要的,它通常称作OID。OID管理器确保为所有对象生成唯一的键值并存储到数据库中。
问题:
我们如何确保把每个对象唯一保存在数据库中,而不管是否和其他对象共享相似的状态?
特定约束:
? 您不会希望在一个数据库中改变键值和重复键值,数据库管理员认为这是非常糟糕的事;
? 增加id有时是人工劳动(artificial),通常需要在表中增加附加的字段;
? 为分布式数据库创建一个唯一键值;
? 为SQL代码明确地标识一条记录而创建一个唯一键值。
解决方案:
提供一个OID管理器,为所有需要存入数据库的对象创建唯一键值,确保所有新创建并需持久化的对象都能得到一个唯一的键值。当一个新对象需要持久化,它将被写入数据库并且有一个唯一的标识生成,这个生成过程要求非常快速且要求确保标识的唯一性。
一种方案是生成随机数,一旦生成一个数,必须检查它是否已经使用。不过当在多个数据库运行时,无法从本地获知,所需时间增加,而且当从多个数据库融合数据时,键值重复的可能性更大了。
另一个方案是在本地表中存放最后使用的数字,并和一个本地且唯一的键值合并起来,这需要每个键值都读取、写入键值表。
一个更好的方法(本例中使用的)是前面一种方法的变种,可以减少将每个键值都写入表的需要[Ambler 97]。当需要一个键值,向一个singleton实例请求,如果这个实例中没有任何数字(例如第一次写入),就从一个表读出一个数字段。一旦返回这个数字,它将立即增加一个特定数(应用程序指定),并写回数据库,给下次其他用户访问用。这个数返回给调用对象并增加1,存储在内存中直到下次需要一个键值。当这个段的数字都用完了,将重新读入一段并重复上面的过程。
可以使用一个键值生成策略来创建这些键,一个数据库或站点可以使用一种算法,而另一个可以使用其他算法,重要的是键值在表中是唯一的,最好在整个数据库或多个数据库中是唯一的。
有些数据库有生成唯一键值的方法,需要强调的是如果你有多个服务器,生成算法不能冲突。你也可以使用一个TCP/IP地址和/或硬盘序列号,跟上其他数字,以确保对象标识的唯一性。同时,有些数据库有生成一序列数字的方法,可以用来作为OID,不管您使用什么算法,确保线程安全是非常重要的。
注:将这个和Kellers和Browns模式语言中的Unique Key模式联系起来。
示例实现:
OID管理器为持久层提供域对象的唯一标识,这个标识可以作为数据库的键值。OID管理器维护一张表,表中有一个数,当它不能向持久层返回一个合法键值时,OID管理器增加这个数并写回表中。本例中,当它没有一个合法数字时,从表中获取一段数字维护,直到数字超过写回表的数。本例中段的大小为10(见下面的increment方法)。
Protocol for Accessors OIDManager (instance)
这个方法返回一个值,这个值是当需要一段新数字时,从数据库获取的数字段的大小。它可以被应用系统在启动和不想再设为10的时候设置。不懂
increment
“返回增加的值。”
(increment isNil)
ifTrue:[increment:=10].
^increment
Protocol for Key Generation OIDManager (instance)
这是本模式的核心方法,从表中读出一个字段值,所有用户访问数据库都使用它。当读取一个值,它立即加上increment属性中存放的值,这个值缺省为10,新数值接着被写回到表中以被下一次使用和下一个用户获取。这个值初始读出并存放在属性中,必要时进行操纵以提供唯一数值,这个数将附加上站点(或数据库)的键值,以创建一个唯一的14或15位数字的数。
readKey
“生成一个唯一键值,使对象存放入数据库中”
| newKey aQuerySpec aResultSet aMaxKey prep |
PersistenceObject beginTransaction.
aQuerySpec := AbtQuerySpec new
statement: ‘ SELECT NUM_SEQ FROM SEQUENCE ‘.
aResultSet := PersistenceObject databaseConnection
resultTableFromQuerySpec: aQuerySpec.
aMaxKey := aResultSet first at: ‘NUM_SEQ’.
newKey := aMaxKey + self increment.
PersistenceObject
executeSql: ‘UPDATE SEQUENCE SET NUM_SEQ = ‘, newKey printString.
PersistenceObject endTransaction.
prep := self class siteKey * self keySize.
self lowKey: prep +aMaxKey.
self highKey: prep + (newKey -1).
^nil
Protocol for Key Retriever OIDManager (instance)
这是获取一个键值的方法,检查这个属性,看是否需要通过上面的方法读出一个数,或是仅仅为这个单一实例增加1并返回它的值。
getKey
“这是获取一个键值的方法。”
self currentKey =0 ifTrue: [self readKey].
self currentKey = self highKey
ifTrue:
[self readKey.
^self currentKey].
self currentKey: self currentKey + 1.
^self currentKey
结论:
? 当写回数据库表时总是增加数字将浪费数字值;
? 当写回数据库表时,如果数字增加不足,将会消耗时间;
? 唯一的单一字段键值可以提高性能,并使SQL编码更简单、更易抽象。
相关或交互模式:
? 一个OID管理器是一个单一实例,所有的持久对象在一个共同的地方生成他们的键值;
? 可以使用一个策略来生成键值。
已知应用:
? Illinois Department of Public Health TOTS 和NewBorn Screening项目;
? 所有OODBMS都使用一个键值生成算法来为保存的对象创建唯一键值;
? PLoP Registraion使用Microsoft Access序列数据库命令来生成唯一键值;
? 在DCOM中,每个接口在运行期通过他们的接口标识(IID)来识别。IID是DCOM为接口生成的一个全局唯一标识(GUID)。GUID有128位,由DCOM API函数CoCreateGuid生成,这个API调用依照由OSF DCE指定的算法,它使用当前日期和时间、网卡ID和一个高频度计数器。基本上,你可以使用工具(例如Microsoft Developer Studio)来获得这些GUID并将他们放入你的DCOM IDL。
? CORBA2.0拥有联合的接口仓库――在多个ORB之间操作的仓库。为避免命名冲突,仓库为全局接口和操作分配唯一的ID(CORBA中称作仓库ID)。一个仓库ID是一个字符串,有三个层次组成,CORBA2.0定义了两个格式:
i) IDL名称有一个唯一的前缀,它的部件组成是:一个IDL字符串,跟着一个由’/’分隔的标识符列表和一个主辅版本号。通过’:’分隔的第一个标识符是一个唯一前缀――java使用同样的方式。例如:
IDL:JoeYoder/Foo/Bar/:1.0
JoeYoder是唯一前缀
Foo是一个模块
Bar是一个接口
ii) DCE通用唯一标识符(UUID),它使用DCE提供的UUID生成器生成――例如DCOM。它的部件通过’:’分隔,第一个部件是DCE字符串,第二个是UUID,第三个是一个版本号(没有辅版本号)。例如:
DCE:100ab200-0123-4567-89ab:1