類型轉換
別名:
數據轉換
類型翻譯
動機:
數據庫值類型並不總是和對象類型直接對應,例如,一個布爾值也許在數據庫存成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