動機:
試想你有一個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將數據庫字段映射到對象屬性中。
類型轉換
別名:
數據轉換
類型翻譯