clientdataset的使用

Delphi做爲一個快速應用開發工具,深受程序員的喜愛。其強大的組件功能,讓程序員能夠輕鬆、高效地完成常見的界面開發、數據庫應用等功能。然而,幫助的相對缺乏,使得許多組件的功能並不爲人們正確地使用,究其原因,仍然是認識上的問題。對於MIDAS開發中的核心部件,TClientDataSet和TDataSetProvider,由於資料的缺乏,人們在網上大多談論的是李維的書籍內容。我有幸在BDN上見到了Cary Jensen的Professional Developer系列文章,詳細闡述了DELPHI的數據庫開發技術。現節選出其中的ClientDataSet部分,與大家共同分享。

ClientDataSet是一個功能強大的類,通過在內存中模擬表格,實現了其它數據集組件所不具備的強大功能。以往只在Delphi和C++ Builder企業版中才提供這個組件,如今,Borland的全部產品(包括最新的Kylix)都集成了TClientDataSet組件。

TClientDataSet從類的繼承關係上來看,是TDataSet這個抽象類的子類,所以我們可以在TDataSet這個抽象層次上對其進行我們熟悉的操作,比如導航、排序、過濾、編輯。要注意的是,TClientDataSet使用了一種全新的技術,它將所有的數據均放在內存中,所以TClientDataSet是個只存在內存中的“虛擬表”,因此對數據庫的操作是非常快的。在PIII 850,512MB的機器上對十萬條記錄進行建索引的操作,花費的時間少於半分鐘。

與一般的數據集組件不同,TClientDataSet使用的技術比較特別,本着高速度、低存儲需求的原則,TClientDataSet的內部使用了兩個數據存儲源。第一個是其Data屬性,這是當前內存數據的視圖,反映了所有的數據改變。如果用戶從數據中刪除一條記錄,則此記錄將從Data中消失,相應地,加入一條新記錄後,此記錄便存在Data屬性中了。

另一個數據源是Delta屬性,故名思義,即增量的意思,這個屬性反映了對數據的改變。無論是向Data屬性新增還是刪除記錄,都會在Delta中記錄下來,如果是修改了Data中的記錄,則會在Delta保存兩條相應的記錄,一條是原始記錄,另一條僅包含修改的字段值。正因爲Delta的存在和TClientDataSet在內存中記錄數據的特點,所有的改變都沒有立即更新加對應的物理存儲中,可以根據這些信息在適當的時候恢復,所以TClientDataSet天生具有緩衝更新功能。

爲了使數據更新回數據存儲源,我們要調用TClientDataSet中對應的方法。如果ClientDataSet與DataSetProvider關聯,那麼僅需調用TClientDataSet的ApplyUpdates方法即可保存數據的更新,但如果TClientDataSet沒有對應的TDataSetProvider存在,而是直接同文件關聯,那麼,這種方式是非常有趣的,我們在BriefCase模型中會再次講解這個問題。此時,如果使用TClientDataSet的SaveToFile和LoadFromFile,都會保留着Delta。調用MergeChangeLog和ClearChanges後,Delta的內容纔會被
清空。只是前者是將Delta的數據同Data結合起來,將改變存儲到物理介質上,而ClearChanges則是一股腦兒全部清空,將數據回覆到原始狀態。大部分的應用都是將TClientDataSet與TDataSetProvider結合使用的。兩者聯合使用的行爲反映了Borland的設計宗旨,就是要提供一個面向分佈式環境的思路。我們下面來慢慢解釋。

當我們將TClientDataSet對象的Active屬性設爲True或者調用其Open方法後,ClientDataSet會向DataSetProvider發送一個取數據包請求。於是DataSetProvider便會打開對應的數據集,將記錄指針指向第一條記錄,然後從頭到尾依次掃描。對於掃描到的每一條記錄,都會將其編碼成一個variant數組,我們通常將它稱之爲數據包。完成掃描後,DataSetProvider會關閉指向的數據集,並將所有的這些數據包傳遞給ClientDataSet。在我提供的演示程序中,你可以清楚地看到這種行爲(畢竟眼見爲實嗎!)。程序主界面右邊的DBGrid連接到一個指向數據庫表的數據源,DataSetProvider即指向此表。當選擇了ClientDataSet | Load菜單項時,你可以看到表格的數據被依次掃描,一旦到達最後一條記錄,表格便會被關閉,右邊的DBGrid被清空,而左邊反映ClientDataSet數據的DBGrid便出顯示出內存中的數據來。由於這個過程會在DBGrid上反映出來,所以不到1000條記錄的取出時間中,大部分都浪費在屏幕的更新顯示上了,你可以選擇ClientDataSet | View Table Loading來禁止顯示,而達到加速的目的。

在上面的描述中,我們沒有提到一個重要的環節,即數據包是如何還原成表格的。那是因爲DataSetProvider會將數據包中的元數據解碼出來,根據元數據(我們可以理解爲數據表的結構)便可以構造出與物理數據表一模一樣的內存虛擬表。但要注意的是,儘管DataSetProvider指向的數據表可能有多個索引,但這些信息是不會放在數據包中的,換句話說,ClientDataSet當中的數據默認情況下是無索引的。但因爲ClientDataSet具有與TDataSet一致的行爲,所以我們可以在此基礎上根據需要重建索引。

在ClientDataSet中的數據被修改後,可以提交給物理數據表持久化這此改變。這個工作便是由DataSetProvider完成的。內部工作原理是:DataSetProvider創建一個TSQLResolver的實例,這個實例會生成要在底層數據上執行更改的SQL語句。詳細地說,就是對修改日誌中的每一條被刪除、插入、更改記錄生成對應的SQL語句。這個語句的生成也可以由用戶控制,DataSetProvider的UpdateMode屬性和ClientDataSet中的ProviderFlags屬性都對SQL語句的生成有影響。

當然,你也可以換一種方式,即採取同單機或C/S結構一樣的數據直接操作機制,繞過SQL語句和緩衝更新機制來修改數據庫。只需將ResolveToDataSet屬性設爲True,那麼DataSetProvider在持久化更新時便不會使用TSQLResolve,而是直接修改物理數據源。即定位到要刪除的記錄,調用刪除語句,定位到修改記錄,調用修改語句。我們可以對演示程序稍加修改,觀察此種行爲。請將演示程序中的DataSetProvider的ResolveToDataSet屬性由False改爲True,運行。在界面中修改數據並且保存,你將會看到右邊的導航按鈕會在瞬間變得可用。

更絕妙的是,Borland考慮到了應用的多樣性,爲我們提供了BeforeUpdateRecord事件,這樣,當DataSetProvider對每個修改日誌的記錄進行操作時,都會觸發此事件,我們可以在此事件中加入自己的處理,如“加密操作”、“商業敏感數據處理”等應用,從而極大地方便了程序員,讓程序員對於數據具有完全的控制能力。分佈式環境的複雜性對數據的存取提出了更高的要求,所以使用事務來保證數據的完整性和一致性是非常必要的,Borland考慮到了這一點,當調用ClientDataSet的ApplyUpdates時,你可以傳遞一個整數值來指明可以容忍的錯誤數量。如果你的數據非常嚴格,則可以傳遞0值,這樣,DataSetProvider在應用修改時便會打開一個事務,如果遇到錯誤,便會回退此事務,修改日誌將保持原樣,並且將出錯的記錄標記出來,最後會觸發OnReconcileError事件。如果傳遞了一個大於0的數,則當出現的錯誤數量小於此指定值時,事務會被提交,發生錯誤而導致提交失敗的記錄會保留在Delta中,而提交成功的記錄會從修改日誌中刪除。若錯誤數量達到指定值,則事務會回退,結果同整數值爲0的情況。如果值爲負數,則會交所以可提交的數據都提交,不可提交的數據仍然保存在修改日誌中,並將出錯記錄標記出來。

雖然,Borland是爲了滿足分佈式編程的需要而設計了TClientDataSet,但在其它類型的編程環境中使用ClientDataSet也具有積極的意義。首先,我們可以看到,由於數據均在內存中進行操作,而且僅在打開數據庫取數據時和將修改持久到回數據庫時,纔有數據庫開銷,其它時間數據庫爲零,這樣就極大地增加了數據庫的負荷,讓數據庫服務器能滿足更多用戶的連接請求。其次,ClientDataSet具有其它數據集所不具備的許多高級功能,這爲程序員進行復雜的編程提供了便利,可以不考慮數據庫本身是否支持這此功能,而讓ClientDataSet去處理這些複雜而繁瑣的細節。最後,ClientDataSet在數據存儲和應用程序間起到一個抽象層的作用。假如你的程序使用了TClientDataSet,那麼如果你以後要更改數據庫存儲機制。比如說由BDE移植到dbExpress,或者從ADO移植到Interbase Express,你的用戶界面和數據控制部分幾乎就不用改變,只需要將DataSetProvider指向新的數據存取組件即可。順便說一句,由於緩衝更新的存在,用戶可能非常厭惡調用ApplyUpdates操作,那麼你可以將此調用放入AfterPost和AfterDelte中,讓用戶的操作更方便。

與TTable、TQuery一樣,TClientDataSet也是從TDataSet繼承下來的,它通常用於多層體系結構的客戶端。TClientDataSet最大的特點是它不依賴於BDE(Borland Database Engine),但它需要一個動態鏈接庫的支持,這個動態鏈接庫叫DBCLIENT.DLL。在客戶端,也不需要用TDatabase構件,因爲客戶端並不直接連接數據庫。
  由於TClientDataSet是從TDataSet繼承下來的,所以,它支持諸如編輯、搜索、瀏覽、糾錯、過濾等功能。由於TClientDataSet在內存中建立了數據的本地副本,上述操作的執行速度很快。也正是由於TClientDataSet並不直接連接數據庫,因此,客戶程序必須提供獲取數據的機制。在Delphi 4中,TClientDataSet有三種途徑獲取數據:
.從文件中存取數據。
.從本地的另一個數據集中獲取數據。
.通過IProvider接口從遠程數據庫服務器獲取數據。
  在一個客戶程序中,可以同時運用上述三種機制獲取數據。
11.1 瀏覽和編輯數據
  和其他數據集構件一樣,可以用標準的數據控件顯示由TClientDataSet引入的數據集,當然,這需要藉助於TDataSource構件。
  由於TClientDataSet是從TDataSet繼承下來的,所以,凡是其他數據集構件支持的功能,TClientDataSet構件也大致具備。不同的是,TClientDataSet能夠在內存中建立數據的副本,因此,TClientDataSet比其他數據集構件增加了一些特殊的功能。
11.1.1 瀏覽數據
  可以用標準的數據控件顯示由TClientDataSet引入的數據集。在運行期,可以調用諸如First、GotoKey、Last、Next和Prior等函數來瀏覽數據。
  TClientDataSet也支持書籤功能,可以用書籤來標記某條記錄,以後就可以方便地找到這條記錄。
  對於TTable、TQuery等數據集構件來說,只能讀RecNo屬性來判斷當前記錄的序號。對於TClientDataSet構件來說,還可以寫RecNo屬性,使某一序號的記錄成爲當前記錄。
11.1.2 CanModify屬性
  TDataSet的CanModify屬性用於判斷數據集中的數據是否可以修改。CanModify屬性本身是隻讀的,也就是說,數據是否能夠修改不取決於應用程序。
  不過,TClientDataSet構件有其特殊性,因爲TClientDataSet已經把數據在內存中建立了副本,因此,應用程序可以決定是否允許修改數據。如果不允許用戶修改數據,只要把ReadOnly屬性設爲True,此時,CanModify屬性肯定返回False。
  與其他數據集構件不同,修改TClientDataSet構件的ReadOnly屬性時,不需要事先把Active屬性設爲True。
11.1.3 取消修改
  TClientDataSet傳輸數據的基本單位稱爲數據包,當前的數據包可以由Data屬性來訪問。不過,用戶對數據的修改並不直接反映到Data屬性中,而是臨時寫到一個日誌即Delta屬性中,這樣做的好處是以後隨時可以取消修改。
  不過,這裏要說明一點,儘管用戶的修改並沒有反映到Data,當用戶在數據控件中看到的卻是最新修改的數據。如果一條記錄被反覆修改了多次,用戶看到的只是最新的數據,但日誌中卻記載了多次。
  要取消上一次的修改,調用UndoLastChange函數。UndoLastChange需要傳遞一個布爾類型的參數叫FollowChange,如果FollowChange參數設爲True,光標就移到被恢復的記錄上,如果FollowChange參數設爲False,光標仍然在當前記錄上。
  ChangeCount屬性返回日誌中記載的修改次數。如果一條記錄被反覆修改了多次,每調用一次UndoLastChange能夠逐級取消上一次的修改。
  UndoLastChange只能取消上一次的修改,如果想一下子取消所有的修改,首先要選擇一個記錄,然後調用RevertRecord。RevertRecord將從日誌中取消所有對當前記錄的修改。
  TClientDataSet還有一個SavePoint屬性,它能把當前的編輯狀態保存起來,以後隨時可以返回當時的狀態。例如,可以這樣保存當前的狀態:
  BeforeChanges := ClientDataSet1.SavePoint;
  以後,可以這樣來恢復當時的狀態:
  ClientDataSet1.SavePoint := BeforeChanges;
  應用程序可以保存多處狀態,可以恢復其中一個狀態,不過,一旦某個狀態被恢復,在其之後的狀態就無效。
  如果要一下子取消日誌中記載的所有修改,可以調用CancelUpdates函數。CancelUpdates將把日誌清空,取消所有的修改。
  如果LogChanges屬性設爲False,用戶對數據的修改就會直接反映到Data屬性中。
11.1.4 合併修改
  要把日誌中記載的修改合併到Data屬性中,有兩種方式,具體使用哪一種方式,取決於應用程序獲取數據的機制。不過,不管是哪種機制,合併後,日誌自動被清空。
  對於一個從文件中獲取數據的程序來說,只要調用MergeChangeLog函數,就把日誌中記載的修改合併到Data屬性中。不用擔心其他用戶同時修改了數據。
  對於一個從應用服務器獲取數據的程序來說,就不能調用MergeChangeLog來合併數據,而要調用ApplyUpdates函數,ApplyUpdates會把日誌中記載的修改傳遞給應用服務器,待應用服務器成功地把數據更新了數據庫服務器後,纔會合併到Data屬性中。
11.1.5 糾錯
  TClientDataSet支持糾錯功能。一般情況下,需要自己建立糾錯規則,以便對用戶輸入的數據進行糾錯。
  此外,如果獲得了IProvider接口的話,還可以從遠程服務器引入糾錯規則。
  有時候,客戶端可能需要暫時禁止糾錯,因爲客戶端從應用服務器檢索數據是分階段進行的,在所有的數據檢索完畢之前,有些糾錯規則很可能會報錯。
要暫時禁止糾錯,可以調用DisableConstraints,要重新允許糾錯,可以調用EnableConstraints函數。DisableConstraints和EnableConstraints實際上都是作用於一個內部的計數。
11.2 索 引
  使用索引有這麼幾個好處:
.在數據集中定位記錄比較快。
.能夠在兩個數據集之間建立Lookup或Master/Detail關係。
.可以對記錄排序。
  在多層體系結構中,當客戶程序從應用服務器檢索數據時,它同時獲得了默認的索引。默認的索引叫DEFAULT_ORDER,可以使用這個索引排序,但不能修改或刪除這個索引。
  除了默認的索引外,TClientDataSet還對日誌中記載的記錄自動建立了一個副索引叫CHANGEINDEX。與DEFAULT_ORDER一樣,不能修改或刪除這個副索引。
  另外,還可以使用數據集中已建立的其他索引,或者自己建立索引。
11.2.1 創建一個新的索引
  要創建一個新的索引,可以調用AddIndex。AddIndex需要傳遞若干個參數:
  一是Name參數,用於指定索引名。在運行期切換索引時需要用到索引的名稱。
  二是Fields參數,它是一個字符串,用於指定索引中的字段名,彼此之間用分號隔開。
  三是Options參數,用於設置索引的選項,包含ixDescending元素表示按降序排列,包含ixCaseInsensitive元素表示大小寫不敏感。
  四是DescFields參數,它也是一個字符串,用於指定若干個字段名,這些字段將按照降序排列。
  五是CaseInsFields參數,它的作用與DescFields參數類似,包含在CaseInsFields參數中的字段將對大小寫不敏感。
  六是GroupingLevel參數,用於指定分組級別,其值不能超過索引中的字段數。
  下面的代碼創建了一個索引:
If Edit1.Text <> '' and ClientDataSet1.Fields.FindField(Edit1.Text) then
Begin
ClientDataSet1.AddIndex(Edit1.Text+'Index',Edit1.Text,  
  [ixCaseInsensitive],'','',0);
ClientDataSet1.IndexName := Edit1.Text + 'Index';
End;
爲了避免創建一個索引,可以臨時用IndexFieldNames屬性來指定若干個字段,讓數據集按這些字段排序。
11.2.2 刪除和切換索引
  要刪除一個先前創建的索引,可以調用DeleteIndex並指定要刪除的索引名稱。注意:DEFAULT_ORDER和CHANGEINDEX不能刪除。
  如果建立了多個索引,可以任意選擇其中的一個索引,這就要用到IndexName屬性。
11.2.3 用索引把數據分組
  選擇了一個索引後,數據集將自動按其中的字段進行排序。這樣,臨近的記錄往往在關鍵字段上含有相同的值。例如,假設有一個表是這樣的:
SalesRep Customer OrderNo Amount
1      1     5    100
1      1     2    50
1      2     3    200
1       2     6    75
2      1     1    10
2      3     4    200
  可以看出,SalesRep字段的值有重複的。對於SalesRep字段的值爲1的來說,Customer字段的值也有重複的。這就是說,可以按SalesRep字段分組,進而再按Customer字段分組。顯然,這裏的分組級別是不同的,按SalesRep字段建立的分組屬於第一級,按Customer字段建立的分組屬於第二級。實際上,分組級別取決於字段在索引中的順序。
  TClientDataSet可以決定是否按照分組級別來顯示記錄的值。例如,也許想以下面這種形式顯示數據:
SalesRep Customer OrderNo Amount
1      1    5    100
           2    50
       2    3    200
           6    75
2      1    1    10
2      3    4    200
  要判斷當前記錄某一級的什麼位置,可以調用GetGroupState函數。GetGroupState函數需要傳遞一個參數,用於指定分組級別。
11.3 計 算 字 段
  與其他數據集一樣,也可以在TClientDataSet建立的數據集中增加計算字段。計算字段的值是基於同一個記錄中的其他字段計算出來的。
  在其他數據集中,只要用戶修改了數據或當前記錄發生改變,就會觸發OnCalcFields事件,換句話說,計算字段的值就被計算一次。
  TClientDataSet引入了“內部計算字段”的概念。與一般的計算字段不同的是,內部計算字段的值將隨其他字段的值一起存取,這樣,只有當用戶修改了數據纔會觸發OnCalcFields事件,如果僅僅改變了當前記錄,不會觸發OnCalcFields事件。也就是說,內部計算字段的值需要重新計算的機會大大減少。
  在處理OnCalcFields事件的句柄中,首先要判斷State屬性。如果State屬性返回dsInternalCalc,此時需要計算內部計算字段的值。如果State屬性返回dsCalcFields,此時需要計算一般的計算字段的值。
11.4 統 計 值
  TClientDataSet增加了統計的功能,它可以基於分組自動計算總和、平均、計數、最大、最小值。當用戶編輯數據時,這些統計值會自動跟着變化。
11.4.1 指定統計方式
  要指定怎樣進行統計,就要用到Aggregates屬性。這個屬性是一個TAggregates對象,它用於管理一組TAggregate對象。
  在設計期,可以單擊Aggregates屬性邊上的省略號按鈕打開如圖11.1所示
的編輯器。
  圖11.1 管理一組TAggregate對象
  單擊按鈕可以增加一個TAggregate對象,單擊按鈕可以刪減一個TAggregate對象,單擊按鈕可以把TAggregate對象前移,單擊按鈕可以把TAggregate對象後移。
  可以用字段編輯器專門創建一個用於表達統計值的字段,該字段的類型必須是“Aggregate”。Delphi 4會自動創建一個TAggregate對象,並加到Aggregates屬性中。選擇一個TAggregate對象,Object Inpector將顯示該對象的屬性。
  其中,Expression屬性用於指定統計表達式,例如:
Sum(Field1)
  也可以是比較複雜的表達式:
Sum(Qty * Price) - Sum(AmountPaid)
  在表達式中,可以使用下列統計運算符:
.Sum計算一組數據的總和。
.Avg計算一組數據的平均值。
.Count計算一組數據中的非空值的個數。
.Min計算一組數據的最小值。
.Max計算一組數據的最大值。
  除了上述幾個統計運算符外,還可以使用過濾條件中所能使用的運算符,但不能嵌套。在一個表達式中,可以混合出現幾個統計值或常量,但不能混合出現統計值和字段。
  Sum(Qty * Price){合法}
  Max(Field1) - Max(Field2){合法}
  Avg(DiscountRate) * 100{合法}
  Min(Sum(Field1)){非法,不能嵌套}
  Count(Field1) - Field2{非法,統計值和字段不能混合出現在一個表達式中}
11.4.2 指定分組
  默認情況下,統計值是基於數據集中所有的記錄計算出來的。不過,也可以針對一部分記錄計算統計值,這就需要事先建立分組。
  前面在介紹索引時已經提到分組的概念。可以通過IndexName屬性和GroupingLevel屬性來選擇使用哪個索引以及最大的分組級別。
  例如,假設有一個表是這樣的:
SalesRep Customer OrderNo Amount
1      1     5    100
1      1     2    50
1      2     3    200
1       2     6    75
2      1     1    10
2      3     4    200
  如果要按SalesRep字段分組,並且指定其中的第一級,程序代碼應當這樣寫:
Agg.Expression := 'Sum(Amount)';
Agg.IndexName := 'SalesCust';
Agg.GroupingLevel := 1;
Agg.AggregateName := 'Total for Rep';
11.4.3 怎樣獲取統計值
  要獲取統計值,可以調用TAggregate對象的Value函數。如果統計值是基於數據集中所有的記錄計算出來的,隨時可以調用Value函數。如果統計值是基於分組計算出來的,必須保證當前記錄正好位於該分組內。因此,在調用Value之前,最好先調用GetGroupState函數看看當前記錄是否位於該分組內。
  要在數據控件中顯示統計值,必須事先在字段編輯器中創建一個永久字段對象,該字段的類型必須是Aggregate。
11.5 數 據 包
  通過Data屬性可以訪問客戶程序從應用服務器檢索到的數據。程序示例如下:
Procedure TForm1.Button1Click(Sender: TObject);
Begin
ClientDataSet1.Data := ClientDataSet1.Provider.DataRequest(FilterEdit.Text);
End;
11.5.1 直接對Data屬性賦值
  前面講過,客戶程序既可以通過IProvider接口獲取數據,也可以從另一個數據集獲取數據,後者就是通過Data屬性賦值的。程序示例如下:
  ClientDataSet1.Data := ClientDataSet2.Data;
  一旦Data被賦值,就可以用標準的數據控件顯示這些數據。
  注意:當從另一個數據集獲取數據時,另一個數據集的日誌也將被複制過來,但不包括原來的範圍和過濾條件。
  如果要從另一個基於BDE的數據集中獲取數據,可以通過數據集構件的Provider屬性,程序示例如下:
  ClientDataSet1.Data := Table1.Provider.Data;
  如果要從一個自定義的數據集獲取數據,首先要創建一個臨時的TProvider構件,然後設置其DataSet屬性指定這個自定義的數據集。程序示例如下:
TempProvider := TDataSetProvider.Create(Form1);
TempProvider.DataSet := SourceDataSet;
ClientDataSet1.Data := TempProvider.Data;
TempProvider.Free;
11.5.2 在數據包中加入自定義的信息
  可以把自定義的信息加到數據包中。當把數據保存到文件或流中時,這些自定義的信息也將保存到文件或流中。如果把數據包直接賦值給另一個數據集的話,這些自定義的信息也將被複制。
  要把自定義的信息加到數據包中,可以調用SetOptionalParam函數。要從數據包中檢索自定義的信息,可以調用GetOptionalParam。程序示例如下:
Procedure TAppServer.Provider1UpdateData(Sender: TObject; DataSet: TClientDataSet);
var
WhenProvided: TDateTime;
Begin
WhenProvided := DataSet.GetOptionalParam('TimeProvided');
...
End;
11.5.3 克隆另一個數據集
  調用TClientDataSet的CloneCursor函數可以獲得一個數據集的完全相同的副本。它與直接通過Data屬性賦值是有區別的。
  區別之一:數據在兩個數據集之間是共享的,修改其中一個將同時修改另一個。
  區別之二:除了數據外,CloneCursor函數還複製了一些屬性和事件,這取決於Reset和KeepSettings參數怎樣設置。
   CloneCursor函數需要傳遞三個參數,其中,Source參數指定源數據集,Reset參數和KeepSettings參數用於設置除了數據外是否還要複製下列屬性和事件:Filter、Filtered、FilterOptions、OnFilterRecord、IndexName、MasterSource、MasterFields、ReadOnly、RemoteServer、ProviderName、Provider。
  如果Reset和KeepSettings參數都設爲False,源數據集的上述屬性和事件都將被複制給目標數據集。如果Reset參數設爲True,目標數據集的上述屬性和事件都將被清空。如果Reset參數設爲False,而KeepSettings參數設爲True,目標數據集的上述屬性和事件不變,不過,必須保證這些屬性和事件與克隆後的數據相容。
11.6 與應用服務器通訊
  在多層體系結構中,客戶程序通過IProvider接口與應用服務器交換數據。這一章介紹怎樣在客戶端獲得IProvider接口、怎樣嚮應用服務器傳遞參數、怎樣嚮應用服務器請求數據、怎樣把用戶對數據的修改寫到數據庫中。
11.6.1 怎樣在客戶端獲得IProvider接口
  在單層應用程序以及工作在“公文包”模式下的多層應用程序中,不需要用到IProvider接口。而在多層體系結構中,客戶程序要與應用服務器交換數據,首先必須獲得IProvider接口,這就要用到RemoteServer屬性和ProviderName屬性。
  RemoteServer屬性用於指定客戶端的MIDAS連接構件。MIDAS連接構件又稱Data Broker,用於建立和維護與應用服務器的連接。
  在設計期,正確設置了RemoteServer屬性後,就可以在對象觀察器中爲ProviderName屬性選擇一個值,實際上就是選擇應用服務器上的一個TProvider構件。
11.6.2 嚮應用服務器傳遞參數
  客戶程序可以嚮應用服務器傳遞參數,這些參數實際上是傳遞給應用服務器上的TQuery構件或TStoredProc構件。既可以在設計期也可以在運行期設置參數。
  在設計期,可以單擊Params屬性邊上的省略號按鈕,打開一個如圖11.2所示的編輯器。
  圖11.2 設置參數
  單擊按鈕可以增加一個參數,單擊按鈕可以刪減一個參數,單擊按鈕可以把一個參數前移,單擊按鈕可以把一個參數後移。
  選擇一個參數,對象觀察器將顯示該參數(TParam對象)的屬性。
  在運行期可以調用TParams的CreateParam函數來創建一個參數。例如,下面的代碼創建了一個參數叫CustNo,它的使用類型是ptInput,數據類型是ftInteger,它的值設爲605。
With ClientDataSet1.Params.CreateParam(ftInteger, 'CustNo', ptInput) Do
AsInteger := 605;
  設置好參數以後,如果TClientDataset的Active屬性是False,只要把Active屬性設爲True,這些參數將被自動傳遞給應用服務器。如果Active屬性已經爲True,就要調用SendParams函數把參數傳遞給應用服務器。
  注意:傳遞給應用服務器的參數必須與TQuery構件或TStoredProc構件的參數匹配,包括名稱、數據類型和參數類型。
11.6.3 怎樣嚮應用服務器請求數據
  TClientDataSet提供了兩個屬性和三個方法,用於怎樣嚮應用服務器請求數據:
  一是FetchOnDemand屬性。如果這個屬性設爲True,TClientDataSet會根據需要自動檢索附加的數據包,例如BLOB字段的值或者嵌套表的內容。如果這個屬性設爲False,程序需要顯式地調用GetNextPacket才能獲得這些附加的數據包。
  二是PacketRecords屬性,用於設置一個數據包中最多可容納的記錄數,設爲-1表示一個數據包可以容納數據集的所有記錄。
  三是GetNextPacket函數,用於嚮應用服務器檢索下一個數據包,並把檢索到的數據包添加到前一次檢索到的數據包的後面。這個函數返回實際檢索到的記錄數。
  四是FetchBlobs過程,用於從應用服務器檢索BLOB字段的值。如果FetchOnDemand屬性設爲True,就沒必要調用FetchBlobs函數。
  五是FetchDetails過程,用於檢索嵌套表中的數據。如果FetchOnDemand屬性設爲True,就沒必要調用FetchDetails函數。
11.6.4 更新數據庫
  在多層體系結構中,用戶在客戶端修改了數據後,需要把最新的數據寫到數據庫中,這就要調用TClientDataSet的ApplyUpdates函數。
  ApplyUpdates只需要傳遞一個參數叫MaxErrors,用於指定一個整數,當遇到無法更新的記錄超過這個數時,此次更新就中止。如果MaxErrors參數設爲0,表示只要遇到一個錯誤更新就中止,客戶端的日誌保持不變。如果MaxErrors參數設爲-1,當應用服務器發現有錯誤的記錄,就嘗試更新下一個記錄,等所有的記錄都嘗試過以後才返回。
  ApplyUpdates會自動調用Reconcile函數,進而調用應用服務器上的TProvider構件的ApplyUpdates函數去更新遠程的數據庫服務器。沒有被DBMS服務器認可的記錄通過Reconcile返回給客戶端,此時將在客戶端觸發OnReconcileError事件讓您更正錯誤。最後,ApplyUpdates函數返回仍然沒有被認可的記錄數。
11.7 在文件中存取數據
  要從文件中讀取數據,可以調用LoadFromFile函數。LoadFromFile函數需要傳遞一個參數,用於指定文件名。文件名應包含完整的路徑。如果客戶程序總是從一個固定的文件中讀取數據,可以設置FileName屬性指定一個文件名,以後,當TClientDataSet引入的數據集打開時,就自動從這個文件中讀取數據,不需要調用LoadFromFile。
  要從流中讀取數據,可以調用LoadFromStream。LoadFromStream需要傳遞一個參數,用於指定一個流對象。
  注意:LoadFromFile(LoadFromStream)只能從先前用SaveToFile(SaveToStream)保存的文件中讀取數據。
  要把數據保存到文件中,可以調用SaveToFile函數。SaveToFile需要傳遞一個參數,用於指定文件名。如果指定的文件已存在,文件中的數據將被覆蓋。如果客戶程序總是把數據保存到一個固定的文件中,可以設置FileName屬性指定一個文件名,當TClientDataSet引入的數據集關閉時,就自動把數據保存到這個文件中,不需要調用SaveToFile。
  要把數據保存到流中,可以調用SaveToStream。SaveToStream需要傳遞一個參數,指定一個流對象。
  注意:當把數據保存到文件或流中時,日誌中記載的修改仍然保留。這樣,當下次調用LoadFromFile或LoadFromStream讀取數據時,仍然可以恢復原來的數據。
嵌套ClientDataset:
1: 當ClientDataset.packetRecords=-1時,不論客戶端的ClientDataset.fetchOnDemand和服務端的DatasetProvider.FetchDetailOnDemand如何設置均會導致速度下降到難以忍受。
1.1原因解析:因爲服務端datasetProvider指向的主表Dataset會在剛打開時就爲每一條紀錄整理其Detail
數據(循環發送Sql語句到數據庫,並cache到子表的dataset,供以後的Filter使用)。所以服務端的Dataset打開會非常慢。

1.2:解決方案:只有返回一條以編輯或增加一條主表紀錄時才使用嵌套ClientDataset。其他瀏覽時應動態刷新Detail控件。
1.3或者設定ClientDataset.packetRecords爲一個比較小的值(100以內)。

1.3 clientdataset的fetchonDemand屬性。基本不影響速度(它不影響無服務端封裝Detail的行爲)。它只設定clientdataset.next時是否自動GetNextPacket.
1.4 DatasetProvider.FetchDetailOnDemand;使用Table作爲子表會影響一倍左右的速度(1條主表紀錄對應平均4條子表紀錄時。使用query作子表時速度基本一樣。)
1.5 不論ClientDataset.packetRecords如何設置,服務端的主表qry都從數據庫中一次取出全部紀錄。
1.6 慎用ClientDataset.IndexName,它會導致讀入所有紀錄到客戶端。 
2、結論:對於存在主子表顯示或編輯要求的客戶端需要使用PacketRecords屬性設爲較小值。(此類客戶端一般不會要求顯示太多數據)。可以將服務端的DatasetProvider.FetchDetailOnDemand設置爲false影響不大,還可簡化客戶端代碼。對於統計查詢的數據不要使用主子表關係。Tips:在子表的Sql中可以返回連接表數據。並設置相關字段爲not pfInUpdate,可以簡化客戶端代碼(不用使用計算字段)並能夠編輯更新子表數據。


3.所有需要更新的Clientdataset均不能與其他的Clientdataset共享一個DatasetProvider.

4、設置PackageRecordcount<>-1的Clientdataset不能與其他的Clientdataset共享一個DatasetProvider.

5、對於浮點數可能會導致更新ClientDataset出現"Record Changed by Another user"錯誤,這是由於float截斷導致的。將對應的DatasetProvider的Updatemode=whereKeyOnly.將聯繫的
query字段PKNO的providerFlag設置爲pfInkey.

6、怎樣更新一個多表連接的結果集?i.設置datasetProvider.onGetTableName事件,設定要更新的表。ii.將相關的query字段(來自其它表的字段)的provideFlag去除pfInputDate屬性。

7、將編輯修改用的clientdataset和查詢瀏覽用的clientdataset的provider分開的原因:使客戶端的編輯窗口比較獨立於其他窗口,降低耦合度,提高編輯瀏覽詳細窗口的通用性。
8、不將編輯修改用的clientdataset和查詢瀏覽用的clientdataset的provider分開的原因:修改後不用重新刷新查詢的clientdataset。不用重新定義計算字段。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章