动机:
试想你有一个Patient类,有Name和Address部件,当你读取一个Patient,必须同时读取Name和Address。写入一个Patient到数据库中将有可能写入一个Name和Address对象。他们是否有同样的接口去读取和写入呢?也许有些对象需要不同的接口?我们能否给出完全同样的接口,如果可以,是什么?
任何被持久化的对象都要对数据库进行读取和写入,对新创建的对象,它的值也会被持久化,另外,对象也可以从持久存储中删除,因此,如果一个对象需要持久化,至少要提供最小的操作集合,他们是创建、读取、更新、删除。
问题:
一个持久对象最小的操作集合是什么?
特定约束:
? 保存在数据库中所有的对象需要一个装载自己、保存自己的机制;
? 在一个地方放置读取和写入的代码有助于对象的演进和维护;
? 如果类只实现同样且小的接口,可以很容易将他们组成嵌套的类。
解决方案:
为持久对象提供基本的CRUD(创建、读取、更新和删除)操作。其他需要的操作如loadAllLike:或loadAll。重要的是要提供足够多的信息能够从数据库实例化对象,并保存新建的或改变了的对象。
如果所有域对象都有一个共同的PersistentObject超类,那么这个超类可以定义CRUD操作,而所有的域对象能够从它继承,如有必要,子类可以重载他们以提高性能。
如果持久层是使用中间人实现的,那么CRUD操作也由中间人实现,不论什么情况,持久层必须生成SQL代码来读取和写入域对象。这样,每个域对象必须能够获得必要SQL代码的描述,来访问CRUD操作的数据库。CRUD和SQL代码描述紧密合作,确保这些操作能有效持久化域对象。
示例实现:
前面阐述的PersistentObject提供了标准接口,一组基本的操作来映射对象到数据库,保存、装载等。这些方法从PersistentObject继承,访问CRUD操作。有些CRUD方法需要在域对象中重写。AbtDBM*数据库部件提供了executeSql: 方法,让数据库执行SQL语句并返回值。updateRowSql和insertRowSql将在下面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语句(insert、update或delete),一旦决定,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]
deleteAsTransaction
“从数据库中删除自己”
self isPersisted ifTrue: [self basicDelete].
^nil
basicDelete
“在数据库中触发删除SQL语句”
self class
executeSql:( ‘DELETE FROM ‘,self class table, ‘ WHERE ID_OBJ=’,
(self objectIdentifier printString)).
结论:
? 一旦你的对象模型和数据模型分析完毕,分析结果可以用CRUD实现,提供一个性能优化的方案,使开发人员从性能优化的考虑中隔离开来。注:如果你的对象模型和数据模型分析完毕,你能够为你的数据库提供优化的性能方案来实现CRUD操作,以向应用开发者隐藏实现细节。
? 基于多少行或何种类型数据(动态、静态或介于两者之间)来获取数据的灵活性是有必要的;
? 简单的实现数据保存到数据库,应用开发人员无需决定是插入还是更新对象;
? 如果对象模型和数据模型没有很好的分析,CRUD将引起子优化性能的问题。这将使开发者的工作变困难,不得不尝试其他的方法。
相关或交互模式:
? 事务管理器为这些操作提供事务支持;
? CRUD和SQL代码协作,生成必要的数据库调用。
已知应用:
? Illinois Department of Public Health TOTS 和NewBorn Screening 项目
? ObjectShare的VisualWorks Smalltalk ObjectLens[OS 95]使用一个CRUD,以定义如何操纵简单数据对象。VisualAge Smalltalk[VA 98]也在他们的AbtDbm*应用系统中使用CRUD。
SQL代码描述
别名:
查询、更新、插入和删除代码定义
对象查询语言(OQL)描述
通用查询语言(CQL)描述
结构查询语言(SQL)描述
动机:
有的地方,不得不编写从数据库中读取、更新、插入和删除值的SQL代码以保持对象值和持久存储值的一致性。再一次看看一个拥有Name和Address部件的Patient类,SQL代码需要读取和写入Patient的值,同时也必须存储Name和Address的SQL。一方面,你可以硬编码SQL,来读取和写入数据库,另外你也可以在一个共同的地方存储值,开发一个存放对象到数据库的映射的结构映射,并在运行期动态生成SQL。
问题:
在什么地方存储用来生成CRUD操作所需的必要SQL语句的实际描述?
特定约束:
? 当访问一个关系型数据库,对数据库访问的SQL代码必定在某处出现;
? 当域模型增加时,SQL代码的数量也随之增加;
? 编写有效的SQL代码需要你对数据模型和数据库有很深的了解;
? 域模型有可能在一个应用的生命周期内频繁变动;
? SQL代码可以放置在数据库访问需要的任何地方;
? 重复的相似SQL代码可能引起维护的问题;
? 从元数据生成SQL代码能够隐藏一个对象访问开发者框架的细节,但是有一个性能和维护之间的平衡。
解决方案:
提供一个开发者描述SQL代码的地方,以维护对象和持久存储之间的一致性。至少域对象需要知道如何执行CRUD操作(创建、读取、更新和删除)。必要的处理CRUD操作的SQL代码需要在某个地方定义。
维护对象值和持久存储值的一致性非常重要,同时,提供一个手段,让饱受煎熬的程序员尽可能不会在修改完一个域对象后忘了提交一个Update语句也是同样重要的。
这个模式可以以多种方式实现,但要点是SQL语句是封装起来的并且要很容易和持久对象关联起来。SQL代码和域对象紧密关联,那么开发者修改了一个域对象而忘了更新SQL语句的可能性就不大了。
实现二者紧密关联的一个方式就是实实在在地为每个操作编写完全的SQL代码,然后让持久层从域对象读取SQL代码,创建数据库联接并执行数据库调用,中间人和对象都能够从域对象生成SQL代码。另一个方式是提供一个对象查询语言(OQL),它是对CRUD所需必要操作的描述,OQL将被翻译成对数据库必要的调用。另外,还可以使用元数据(Metadata)[Foote&Yoder 1998]来描述CRUD操作,一个CRUD操作翻译元数据来构建适当的SQL,大多数商业性框架都使用这种方式,他们通过某种Schema Map[Foote&Yoder 1998]构建这个结构,这些商业性框架大多提供一种可视化语言来构建和操纵这些查询。使用元数据和Schema Maps构建的实现和维护会变的复杂些,并且会产生一些非优化的查询,但是他们能使开发者易于描述域对象和数据库之间的映射,特别是当有可视化语言辅助这一工作时。
如果你正实现一个更大规模的系统,你拥有上千行的SQL代码和动态的SQL,他们不够快,你也许想替代他们或修改他们,例如调用存储过程、预编译的SQL、实现推拉技术或是高速缓存技术,这叫做优化查询模式[Keller 97-2]。
示例实现:
当你使用一个数据库,你必须编写一些SQL语句来获取、插入、更新和删除记录,例如最简单的形式,如下:
SELECT * FROM table_name.
INSERT INTO table_name (column_name) VALUES (values)
UPDATE table_name SET column_name = xyz WHERE key_value
DELETE FROM table_name
这看起来很简单,但是如何从一个对象得到值放置到这些语句中呢?
你能够使用一个字符流来放置语句中固定部分和对象的属性,或者,那样可以定义你想获取的列。
aStream nextPutAll: ‘SELECT’;
nextPutAll: column_names;
nextPutAll: ‘FROM ‘;
nextPutAll: table_name.
或者
aStream nextPutAll: ‘INSERT INTO ‘;
nextPutAll: table_name;
nextPutAll: (column_names);
nextPutAll: ‘VALUES ‘;
nextPutAll: (values).
下面是一Name类实例方法的例子,Name类在我们的例子中是一个域对象,名称和地址是管理类应用系统中大多都有的。这是用一个非常小的复杂对象演示持久层的简单例子,如上所述,SQL语句并不非得硬编码不可,如果你的数据库和你的对象模型非常相似,那么,可以从一个数据库schema来生成这些语句。
Protocol for SQLCODE (instance)
这些方法为持久层提供实际的SQL语句,他们将被送到数据库中执行,这个SQL语句构成一个字符流并传入到持久层执行。这个例子演示了类型转换器和表管理器的使用,他们将在本文的后面章节详述。
这些方法构建实际被送到数据库的SQL语句,为了性能考虑,使用了写入字符流而不是字符拼接。
insertRowSql
“返回一个插入SQL语句,语句中包含了从对象得来的值。”
| aStream |
aStream := WriteStream on:(String new).
aStream nextPutAll: 'INSERT INTO ';
nextPutAll: self class table;
nextPutAll:' (ID_OBJ,
ID_OBJ_OWN,
NAM_FST,
NAM_LST,
NAM_MID,
EML_ADR,
ORG_NAM,
NUM_PHO)
VALUES (';
nextPutAll: (self typeConverter prepForSql:
(objectIdentifier:= (self getKeyValue)));
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:
self owningObject);
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:
(self first asUppercase));
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:
(self last asUppercase));
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:
(self middle asUppercase));
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:(self email));
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:
(self organization));
nextPut: $,;
nextPutAll: (self typeConverter prepForSql:(self phone));
nextPutAll: ')'.
^aStream contents.
updateRowSql
“返回一个更新SQL语句,语句中包含从对象得来的值”
| aStream |
aStream := WriteStream on:(String new).
aStream nextPutAll: 'UPDATE ';
nextPutAll: self class table;
nextPutAll: ' SET NAM_FST=';
nextPutAll: (self typeConverter prepForSql:
(self first asUppercase));
nextPutAll: ', NAM_LST=';
nextPutAll: (self typeConverter prepForSql:
(self last asUppercase));
nextPutAll: ', NAM_MID=';
nextPutAll: (self typeConverter prepForSql:
(self middle asUppercase));
nextPutAll: ', EML_ADR=';
nextPutAll: (self typeConverter prepForSql:(self email));
nextPutAll: ', ORG_NAM=';
nextPutAll: (self typeConverter prepForSql:
(self organization));
nextPutAll: ', NUM_PHO=';
nextPutAll: (self typeConverter prepForSql:(self phone));
nextPutAll: ' WHERE ID_OBJ=';
nextPutAll: (self typeConverter prepForSql:
self objectIdentifier).
^aStream contents.
这个方法为SQL的select语句提供where子句,每个类都有这个方法来决定select语句中where子句可以使用什么。
selectionClause
“得到一个where子句的字符串表示”
| aStream app|
aStream:= WriteStream on:(String new).
( self objectIdentifier isNil )
ifFalse: [ aStream nextPutAll: 'ID_OBJ=';
nextPutAll: (self class typeConverter prepForSql:
self objectIdentifier).
^aStream contents ].
( self owningObject isNil )
ifFalse: [ aStream nextPutAll: 'ID_OBJ_OWN= ';
nextPutAll: (self typeConverter prepForSql: self owningObject)].
^aStream contents.
Protocol for SQLCODE (class)
这些方法为上面的方法提供表名,并定义在上面产生的where子句中,哪些列将从数据库返回。
table
“从表管理器返回表名。”
^TableManager gettable: ‘EXAMPLE’
buildSqlStatement: aString
“为对象返回读取的SQL语句。”
| aStream |
aStream := WriteStream on:(String new).
aStream nextPutAll: 'SELECT
ID_OBJ,
ID_OBJ_OWN,
NAM_FST,
NAM_LST,
NAM_MID,
EML_ADR,
ORG_NAM,
NUM_PHO FROM ';
nextPutAll: self table.
((aString isNil) or:[ aString trimBlanks isEmpty])
ifFalse:[aStream setToEnd;
nextPutAll: ' WHERE ';
nextPutAll: aString].
^aStream contents.
Protocol for SQL Code PersistentObject (instance)
每个对象需要向持久层提供SQL代码描述,如果对象不需要部分SQL代码,它应该返回“shouldNotImplement”。
insertRowSql
^self subclassResponsibility
selectionClause
^self subclassResponsibility
updateRowSql
^self subclassResponsibility
结论:
? 灵活性,只返回需要的集合,同时,SQL语句可以被替换成其他的形式,如存储过程或预编译查询等;
? SQL语句的性能可以很容易使用数据库工具来判断。
相关或交互模式:
? SQL代码描述使用翻译器模式[GHJV 95]生成数据库语句;
? SQL代码描述使用构建器模式[GHJV 95]为不同的对象提供相同过程;
? SQL代码描述能够使用元数据[Foote&Yoder 98]来生成SQL语句;
? SQL代码描述需要知道Schema[Foote&Yoder 98]以生成正确的语句;
? SQL代码描述为持久层的CRUD操作生成代码;
? SQL代码描述使用从属性映射方法得来的值生成。
已知应用:
? Illinois Department of Public Health TOTS和NewBorn Screening项目;
? ObjectShare的VisualWorks Smalltalk[OS 95]使用SQL代码描述,用来定义如何为简单数据对象执行CRUD操作。VisualAge Smalltalk也在他们的AbtDbm*应用程序使用SQL代码描述;
? 在GemStone GemConnect[GemConn 98],使用SQL代码描述,向一个关系数据库读取和写入对象值。
属性映射方法
别名:
映射数据库到对象
映射对象到数据库
动机:
当从数据库中得到一行记录,每个列的值必须映射到对象的一个属性或一组属性上,同样,当将值存入到数据库中,一个对象的属性必须以某种方式映射到数据库的字段上。试想一下病人的例子,一个Patient对象有病人的姓名和性别相关联,它们可以从数据库的病人表中读取,同时Patient对象还有一个地址关联者,这个值也许有一个外键,参照另一个对象例如Address对象,这样,当读取一个Patient对象,属性映射必须分别将数据库中放置姓名、性别值的字段映射到Patient对象的name和sex属性上,同时,需要创建一个Address对象,并映射到Patient对象的address属性。
问题:
开发者在哪儿、如何描述数据库值和对象属性之间的映射?
特定约束:
? 对象在属性变量中存放值,而数据库在字段中存放值;
? 一个对象的属性到数据库表字段的映射并不总是一对一的;
? 有些对象需要从多个数据库、多个数据库表中得到值,它们是复杂对象;
? 非面向对象数据并不能很好地表示层次结构和对象类型;
解决方案:
对每个需要持久化的对象,编写一个映射数据库值到对象属性的方法和一个映射对象属性到数据库值的方法。持久层将使用第一个方法把从数据库返回的值存储到相应的对象属性中。同样,当PersistentObject存储时,持久层将使用第二个方法把对象的值送到数据库中。当PersistentObject生成SQL代码时,它将映射这些数据。
这些方法从数据库返回一行并填入到相应对象属性中,有时若干字段会映射到一个属性上,这些方法必须也能得到一个对象的属性值,通过一个数据库写入例程映射到数据库字段上,通常情况下,一个属性映射到一个或多个数据库字段,但有时也会是多个属性映射到一个数据库字段上。还有些情况,属性值可能从不同平台的不同数据库产生,对这种情况,聚合类将从别的数据库装载,返回对象赋予给当前的属性。
通常至少有两组属性映射方法,一个用来从数据库读取值,一是将值写回到数据库。当值已被映射到数据库上,属性映射方法需要提供对类型转换的调用。
元数据可以用来定义属性映射,可以和Schema一起使用,也可以不用它。在这种情况下,翻译器将用来生成属性映射。可视化语言也可以用来描述映射,但这种方法通常很难实现和维护,然而一旦实现了,开发者很容易映射一个对象的属性。
当一个属性被映射到另一个域对象,通常使用代理来延迟初始化,例如上面提到的Patient例子,因为很少需要一个病人的地址信息,一个代理可以用来初始化address属性,以后,无论何时需要访问病人的地址,病人的地址信息将从数据库读出来并创建Address对象,在这个例子中,address属性也被更新成指向新创建的Address对象。
实例实现:
一旦数据从数据库取出,他们将从返回的行移到对象属性中去,返回行(VisualAge中)是一个字典结构,可以按下面方式访问:
aRow at: keyValue.
接着,你要将值赋予给属性:
attribute := (aRow at: keyValue).
或者,如果属性要包含一个其他类实例:
attribute := ((Class new) owningObject:
objectIdentifier; youself) load.
将从数据库装载一个Address类的实例并赋予给属性,如果数据库包含的地址信息在不同平台的不同数据库中,Address类将从其他数据库装载。
下面是Name类的属性映射方法,展示了如何映射数据库行到对象的以及应用类型转换的过程。SQL代码描述模式展示了如何将对象映射回数据库中。
Protocol for Map Attributes (instance)
这些方法从PersistentObject>>read:方法接受一行aRow(一条记录),数据表的每行被传入到一个新的实例(被PersistentObject初始化),行中的每个元素在赋给属性前,进行必要的类型转换,对于复杂对象的情况,属性值通过发送一个PersistentObject>>load:(如果是集合的话,发送PersistentObject>>loadAllLike:)到属性类类型,这个方法使用一个带条件语句SQL代码描述实例。例如,在Name类中,你将发送loadAllLike:消息到Address类,并以objectIdentifier作为参数,这将装载所有的owningObject为同一个Name的地址。
initialize: aRow (Name class)
“从一个数据行初始化一个对象实例。”
objectIdentifier := self typeConverter convertToNumber:
(aRow at: ‘ID_OBJ’).
owningObject := aRow at: ‘ID_OBJ_OWN’.
isPersisted := true.
first := self typeConverter convertToUpperString:
(aRow at: ‘NAM_FST’).
middle := self typeConverter convertToUpperString:
(aRow at: ‘NAM_MID’).
last := self typeConverter convertToUpperString:
(aRow at: ‘NAM_LST’).
email := self typeConverter convertToString:
(aRow at: ‘EML_ADR’).
organization := self typeConverter convertToUpperString:
(aRow at: ‘ORG_NAM’).
phone := self typConverter convertToNumber:
(aRow at: ‘NUM_PHO’).
address := (( Address New)owningObject:
objectIdentifier;yourself) load
结论:
? 当数据库结构改变了,只要改变一个地方;
? 属性名和数据库表字段名无需一致;
? 新进入项目的开发人员很容易对应库表字段名和属性名;
? 随着数据库和域对象演进,属性映射方法需要维护。
相关或交互模式:
? 在映射值到数据库和从数据库映射值都需要进行类型转换;
? 当向持久层请求一个调用来读取和写入数据库时,要生成属性映射方法所需的SQL代码;
? 元数据能够和一个Schema一起,用来定义属性映射,或者有可能使用一个翻译器来生成实际的映射。
已知应用:
? Illinois Department of Public Health TOTS和NewBorn Screening 项目;
? ParcPlace的VisualWorks Smalltalk[OS 95]使用属性映射方法,用以定义如何处理属性和数据对象之间的映射。VisualAge Smalltalk[VA 98]也在他们的AbtDbm*应用系统中使用属性映射方法。
? JDBC中的ResultSet类是一条数据行,他的方法如getInt(), getString(),getDate()为属性映射工作,同时也是读取器的类型转换。在JDBC中映射回数据库,PerparedStatement类构造SQL语句,并以问号‘?’作为参数的占位符,并有一组setInt(), setString(), setDate()等等。
? GemStone GemConnect将数据库字段映射到对象属性中。
类型转换
别名:
数据转换
类型翻译