把數(shù)據(jù)訪問層升級到ado.net有很多好處,其中之一是使用內部數(shù)據(jù)集對象。數(shù)據(jù)集對象基本上是一個不連接的、內存中的數(shù)據(jù)庫的拷貝。數(shù)據(jù)集對象包含一個或者多個數(shù)據(jù)表(datatable),每個數(shù)據(jù)表一般對應于數(shù)據(jù)庫中的一個表。數(shù)據(jù)集提供了很多好處,但也帶來一些問題,特別是可能遇到與數(shù)據(jù)并發(fā)性異常相關的問題。我建立了一個簡單的windows forms顧客服務應用程序,用它來解釋該問題的潛在的缺陷。本文我將介紹該應用程序并演示怎樣解決它所引起的數(shù)據(jù)并發(fā)性問題。
本文建立的顧客服務應用程序示例是使用visual basic .net和sql server 2000建立的,但是由于微軟.net框架組件是語言無關(language-agnostic)的,因此任何與.net框架組件兼容的語言都可以使用。同樣,由于數(shù)據(jù)集對象抽象了數(shù)據(jù)源,數(shù)據(jù)源的實際執(zhí)行并不重要;無論下層的數(shù)據(jù)源是sql server、本地xml文件或者從一個服務中檢索到的數(shù)據(jù),數(shù)據(jù)并發(fā)性異常同樣會出現(xiàn)。
數(shù)據(jù)集的利弊
數(shù)據(jù)集提供了很多好處,例如比起數(shù)據(jù)庫層次,它強化了內存中的完整性規(guī)則。數(shù)據(jù)集對象可以定義和強化表之間的關系和列的約束,確保使用的商業(yè)規(guī)則對數(shù)據(jù)庫沒有缺陷。通過數(shù)據(jù)庫抽象,你能建立單個代碼集合訪問數(shù)據(jù)集對象而不必考慮填充該數(shù)據(jù)集的源數(shù)據(jù)。下層的數(shù)據(jù)源也許是sql server、oracle甚至xml文件。無論下層數(shù)據(jù)源是什么,代碼使用相同的方法與數(shù)據(jù)集交互。這使你能改變下層數(shù)據(jù)源而不改變代碼。
但是使用數(shù)據(jù)集的最大好處是提高了性能。因為數(shù)據(jù)集與下層數(shù)據(jù)庫斷開,代碼將更少作數(shù)據(jù)庫的調用,顯著地提高了性能。你能向數(shù)據(jù)集的多個數(shù)據(jù)表中添加新行、驗證每行的有效性和參照完整性。數(shù)據(jù)適配器(dataadapter)把數(shù)據(jù)集連接到下層數(shù)據(jù)庫,能使用一條命令更新下層數(shù)據(jù)庫。每個表中的所有新行都使用命令加入,以確保所有添加到數(shù)據(jù)庫的行都是有效的。
性能的最優(yōu)化是有代價的。因為數(shù)據(jù)集對象與下層數(shù)據(jù)庫斷開,經(jīng)常有機會出現(xiàn)數(shù)據(jù)沒有超期(out of date)的情況。因為數(shù)據(jù)集不保存活動數(shù)據(jù),只保存當時填充數(shù)據(jù)集的活動數(shù)據(jù)的一個快照,與數(shù)據(jù)并發(fā)性相關的問題就會出現(xiàn)。數(shù)據(jù)并發(fā)性問題出現(xiàn)在多個用戶訪問相同的數(shù)據(jù)并且任何一個用戶沒有其它用戶的信息就能更新數(shù)據(jù)。這就出現(xiàn)了一個用戶偶然更新數(shù)據(jù)而不知道那些數(shù)據(jù)已經(jīng)改變了,不是他在程序中看到的了。幸運的是數(shù)據(jù)集對象擁有捕獲數(shù)據(jù)并發(fā)性問題的內建(built-in)支持,因此應用程序能正確地作出反應。
示例程序
一個虛擬的公司使用該顧客服務應用程序建立顧客訂單,更新顧客的個人信息。有很多客戶銷售代表(csr)在桌面上使用該應用程序。csr使用電話獲取訂單,從顧客那兒收集個人信息和支付信息。顧客記錄保存在數(shù)據(jù)庫中以提高回頭客處理訂單的速度,csr接著建立一個訂單并把產(chǎn)品項添加上去,指定數(shù)量和目前的價格,所有的信息收集后,csr點擊place order按鈕,向數(shù)據(jù)庫中插入顧客和訂單記錄。
csr也使用應用程序執(zhí)行通過電子或者緩慢的郵件發(fā)送給公司的請求。這些請求在csr間均勻分開,在每天早晨發(fā)送給他們,csr通過電話執(zhí)行那些請求。系統(tǒng)設計要提高請求的實現(xiàn)速度,所有的顧客在csr之間共享。顧客的每個請求,無論通過電話或者郵件,都被不同的csr處理,增加了發(fā)生數(shù)據(jù)并發(fā)性問題的機會。
為了提高性能,應用程序在內存中保持了一個用顧客和訂單信息填充的數(shù)據(jù)集對象。因為很多雇員同時使用該應用程序,就會有許多活動數(shù)據(jù)的不連接的快照,它們都在雇員的工作站上。所有顧客的維護、訂單輸入和訂單維護都使用名為dsalldata的數(shù)據(jù)集對象。圖1是建立dsalldata的代碼,它是全局模塊的一部分,因此應用程序中的所有窗體都能使用它。
| const connstring = "server=localhost;database=northwind;uid=sa;pwd=" public conncustsvc as sqlclient.sqlconnection public dacustomer as sqlclient.sqldataadapter public cbcustomer as sqlclient.sqlcommandbuilder public daorders as sqlclient.sqldataadapter public cborders as sqlclient.sqlcommandbuilder public daorderdetail as sqlclient.sqldataadapter public cborderdetail as sqlclient.sqlcommandbuilder public dsalldata as dataset public sub main() conncustsvc = new sqlclient.sqlconnection(connstring) dacustomer = new sqlclient.sqldataadapter("select * from customer", conncustsvc) cbcustomer = new sqlclient.sqlcommandbuilder(dacustomer) daorders = new sqlclient.sqldataadapter("select * from orders", conncustsvc) cborders = new sqlclient.sqlcommandbuilder(daorders) daorderdetail = new sqlclient.sqldataadapter("select * from orderdetail", conncustsvc) cborderdetail = new sqlclient.sqlcommandbuilder(daorderdetail) dsalldata = new dataset() dacustomer.missingschemaaction = missingschemaaction.addwithkey dacustomer.fill(dsalldata, "customer") daorders.missingschemaaction = missingschemaaction.addwithkey daorders.fill(dsalldata, "orders") dsalldata.tables("orders").columns("total").defaultvalue = 0 daorderdetail.missingschemaaction = missingschemaaction.addwithkey daorderdetail.fill(dsalldata, "orderdetail") application.run(new frmcustomermaintenance()) end sub |
建立dsalldata的代碼建立了一個空的數(shù)據(jù)集對象、三個數(shù)據(jù)適配器(dataadapter)和三個命令構造器(commandbuilder)。每個數(shù)據(jù)適配器在適當?shù)谋砩蠄?zhí)行一個簡單的"select *"操作,而命令構造器用需要的剩余信息填充數(shù)據(jù)集,使它有插入(insert)、更新(update)和刪除(delete)的能力。主程序使用數(shù)據(jù)適配器對象和所有三個表中的數(shù)據(jù)填充dsalldata,接著使用customer maintenance窗體開始應用程序。
圖2顯示的是customer maintenance屏幕,它有一個綁定到dsalldata的customers數(shù)據(jù)表的datagrid對象。這個簡單的表格允許csr編輯顧客的任意基本屬性。因為該表格綁定到了customers數(shù)據(jù)表,表格中的任何改變都將自動存儲到數(shù)據(jù)表中。dsalldata將保存這些值,直到csr點擊save changes按鈕明確地告訴窗體更新下層數(shù)據(jù)源為止。
為了輸入訂單,使用圖3中的代碼建立了幾個新行并添加到dsalldata中。首先建立一個order記錄,接著在數(shù)據(jù)表orderdetail中為訂單的每個項建立幾個記錄。當所有必須的行添加到dsalldata后,一個適當?shù)臄?shù)據(jù)適配器的update方法調用將用新行更新下層數(shù)據(jù)源。
private sub createorder() private sub addorderdetail(byval orderid as integer, _ dim dr as datarow |
因為csr同時使用應用程序和更新顧客信息,因此好像任何時候一個csr看到的都是過期的信息。為了防止這種現(xiàn)象,應用程序的設計師決定dsalldata的數(shù)據(jù)緩存要每隔30分鐘刷新一次,以確保csr通常看到正確信息。應用程序設計得很仔細,以確保數(shù)據(jù)刷新只在空閑時段進行,這樣它才不影響性能。數(shù)據(jù)集刷新因此會比30分鐘長一點,這依賴于csr的行為。
該應用程序的數(shù)據(jù)模型很簡單(圖4)。它使用sql server 2000存儲,只包含三個表,顧客表、訂單表、每個訂單的細節(jié)表,并定義了適當?shù)闹麈I和關系以確保參照的完整性。此外,在orderdetail上定義了一個觸發(fā)器來更新orders 表的total列。每次插入、更新或者刪除一個orderdetail記錄,調用觸發(fā)器計算該訂單的最后銷售值,并更新orders表的適當?shù)男小D5是trg_updateordertotal觸發(fā)器的代碼:
| create trigger trg_updateordertotal on [dbo].[orderdetail] for insert, update, delete as declare @orderid int select @orderid=orderid from inserted if @orderid is null select @orderid=orderid from deleted update orders set total= ( select sum(price*quantity) from orderdetail where [email protected] ) where [email protected] |
第一個數(shù)據(jù)并發(fā)性異常
該顧客服務應用程序使用了幾個月沒有任何問題,但是突然產(chǎn)生了一個沒有處理的異常dbconcurrencyexception。本段我將解釋導致該異常的環(huán)境。
第一個使用該應用程序的顧客銷售服務人員joe打開應用程序。這將初始化將數(shù)據(jù)載入數(shù)據(jù)集dsalldata并按每30分鐘一次的周期來刷新數(shù)據(jù)。joe的收件箱中有一堆文件,包括顧客傳真、郵寄或者通過電子郵件發(fā)送的更改請求。他開始處理更改請求,但是經(jīng)常被電話訂單中斷。
其間,第二個客戶銷售服務人員sally到達辦公室并打開了她的應用程序實例。sally的應用程序實例也初始化從sql server中載數(shù)據(jù),包括joe所作的更新。sally也接到了一個顧客改變電話號碼的請求。該顧客先前用電子郵件發(fā)送了地址的改變情況,但是在那時不知道他的新電話號碼,現(xiàn)在要更新記錄了。sally使用customer maintenance屏幕更新顧客的電話號碼。當sally改變datagrid中的電話號碼時,新號碼存儲在dsalldata中。當sally確認其它的顧客信息后,她意識到原地址的改變還沒有處理,因此她更新那些信息并點擊save changes按鈕,將新數(shù)據(jù)發(fā)送到sql server數(shù)據(jù)庫。
joe正在處理相同顧客的原始請求。當他打開customer maintenance屏幕時,應用程序從緩存數(shù)據(jù)集對象中讀入信息。因為sally更新顧客地址時,joe的應用程序沒有自動與數(shù)據(jù)庫同步,因此他的customer maintenance屏幕仍然顯示舊地址。joe使用電子郵件提供的新信息改正了datagrid中顯示的信息,并點擊save changes按鈕。這樣操作后出現(xiàn)了一個錯誤信息"并發(fā)性故障:更新命令影響了0個記錄(concurrency violation: the updatecommand affected 0 records)",應用程序崩潰了。在joe再次打開應用程序時,他發(fā)現(xiàn)地址已經(jīng)更新了,認為他的更改在應用程序崩潰前已經(jīng)完成了。下面就是問題的代碼行:
| private sub butsave_click (byval sender as system.object, _ byval e as system.eventargs) dacustomer.update(dsalldata.tables("customer")) end sub |
實際的異常是dbconcurrencyexception類型產(chǎn)生的,它是數(shù)據(jù)適配器對象內部建立的特定功能的結果(見圖6)。該數(shù)據(jù)適配器設計為把數(shù)據(jù)填充到不連接的對象(例如數(shù)據(jù)集),這樣它在執(zhí)行更新前能自動地檢查數(shù)據(jù)尋找改變。如果下層數(shù)據(jù)被改變了,數(shù)據(jù)適配器將引發(fā)一個dbconcurrencyexception異常而不是執(zhí)行更新。
執(zhí)行完整性檢查是相當直接的,它提高了數(shù)據(jù)表對象的性能,使它能夠保持多個數(shù)據(jù)集合。當數(shù)據(jù)第一次載入數(shù)據(jù)表時,數(shù)據(jù)表中的所有數(shù)據(jù)行(datarow)的datarowversion屬性設置為原始的(original)。當修改了數(shù)據(jù)行中的一列時,該行就被復制一份并標記為當前的(current),標記為原始的行仍然沒有改變。后來的所有對該數(shù)據(jù)的更改都僅僅影響當前行。當為一個數(shù)據(jù)表(或者一個數(shù)據(jù)集中的多個數(shù)據(jù)表)執(zhí)行數(shù)據(jù)適配器的更新方法時,它重復所有的當前行來決定要發(fā)送給下層數(shù)據(jù)源的更新語句。作為datarowversion屬性的補充,數(shù)據(jù)行有一個用于識別行中數(shù)據(jù)狀態(tài)的rowstate屬性。它的可能值為unchanged、added、modified、deleted和detached。
在決定下層數(shù)據(jù)中的哪些行需要更新后,數(shù)據(jù)適配器dscustomer建立更新sql server數(shù)據(jù)庫所需要的sql語句。在圖1中我使用數(shù)據(jù)集和命令構造器對象來建立需要的insert、 update和delete語句。命令構造器對象建立的update語句使用datarowversion值為original的數(shù)據(jù)行副本來識別和更新數(shù)據(jù)庫中的適當行。這就是說,作為使用主鍵值簡單地識別正確行的代替,命令構造器建立一個sql語句來查找與數(shù)據(jù)集中存儲的原始值準確匹配的行。下面的代碼是建立的用于更新顧客電話號碼的update語句示例:
| update customer set phone = @p1 where ((id = @p2) and ((firstname is null and @p3 is null) or (firstname = @p4)) and ((lastname is null and @p5 is null) or (lastname = @p6)) and ((address is null and @p7 is null) or (address = @p8)) and ((address2 is null and @p9 is null) or (address2 = @p10)) and ((city is null and @p11 is null) or (city = @p12)) and ((state is null and @p13 is null) or (state = @p14)) and ((zip is null and @p15 is null) or (zip = @p16)) and ((phone is null and @p17 is null) or (phone = @p18))) |
該update語句使用參數(shù)而不是實際值,但是你能看到行中每列是怎樣檢查的。
識別了準確的行和下層數(shù)據(jù)庫中的原始值后,數(shù)據(jù)適配器就可以安全地更新行了。但是,如果自從數(shù)據(jù)表被填充后數(shù)據(jù)庫中某行的某個列改變了,update語句將失敗,因為數(shù)據(jù)庫中沒有與where條件中的標準匹配的行了。數(shù)據(jù)適配器決定update是否成功很簡單,只需要簡單地檢查數(shù)據(jù)庫中被更新的行的實際數(shù)量。如果沒有行被更新,那么下層數(shù)據(jù)一定被改變或刪除了,就產(chǎn)生一個數(shù)據(jù)并發(fā)性異常。這就解釋了joe試圖更新顧客電話號碼時接收到的有點模糊的錯誤消息:數(shù)據(jù)適配器檢查到的實際錯誤不是下層數(shù)據(jù)改變了,而是沒有記錄被更新,標志著下層數(shù)據(jù)必定被改變了。
解決方法
有兩種途徑解決dbconcurrencyexception問題。第一種是確保它永不重現(xiàn):我可以刪除圖1中代碼使用的sqlcommandbuilder對象,把它們更換為數(shù)據(jù)適配器對象的updatecommand 屬性的sqlcommand對象。我將在commandtext屬性中建立帶有where條件的sql語句,只進行主鍵而不是所有列的過慮。這樣將排除所有并發(fā)性問題(假定主鍵不會改變)。
但是這種技術帶來了幾個問題。首先,很明顯要更多的代碼,因為我還得為每個數(shù)據(jù)適配器的insertcommand和 deletecommand屬性建立sqlcommand對象。另外,如果下層數(shù)據(jù)庫概要(schema)發(fā)生了變動,這些硬編碼將帶來新錯誤。如果使用sqlcommandbuilder對象,應用程序在運行時決定數(shù)據(jù)庫概要,接受任何改變,相應地建立sql語句。這不是解決并發(fā)性問題,而是完全的避免了該問題,使用戶在不知不覺中覆蓋了他人的更改。
try '從數(shù)據(jù)庫中獲取當前值 '現(xiàn)在恢復用戶輸入的值并再次保存 dacustomer.update(dsalldata.tables("customer")) |
try...catch塊捕捉了dbconcurrencyexception并給用戶顯示一個標識該錯誤的消息窗口,給用戶提供一個選擇(圖8所示)。這樣我識別已經(jīng)出現(xiàn)了一個并發(fā)性錯誤并有兩個選擇:我可以檢索下層數(shù)據(jù)并顯示給用戶,強制他們再次作修改,或者我能簡單地使用該用戶指定的改變覆蓋下層數(shù)據(jù)。
private sub updaterow(byval tablename as string, byval id as integer) '建立命令更新獲取新的下層數(shù)據(jù) '打開連接并建立數(shù)據(jù)讀取器(datareader) '將新數(shù)據(jù)從數(shù)據(jù)庫復制到數(shù)據(jù)行 '接受數(shù)據(jù)行中的改變 |
如果用戶決定查看新的下層改變并放棄他們的更改,你只需要簡單地刷新存儲在customer數(shù)據(jù)表中的數(shù)據(jù)。因為datagrid綁定到數(shù)據(jù)表,該表格將自動地顯示新數(shù)據(jù)。為了刷新數(shù)據(jù),你有兩種選擇:第一種是使用數(shù)據(jù)適配器dacustomer的fill方法來簡單地填充整個數(shù)據(jù)表。雖然這種技術能夠實現(xiàn),但是它的花費太大,因為本來你只需要刷新一行。我建立了一個叫updaterow的程序,它僅僅讀入有問題的行的數(shù)據(jù)(圖9)。使用updaterow程序時要注意我已經(jīng)在參數(shù)集合中定義了被找到的數(shù)據(jù)行是單個的、整型關鍵字的列,如果該表有不同數(shù)據(jù)類型或者復合鍵,你必須重載updaterow來處理特定鍵的需求。在數(shù)據(jù)行和/或數(shù)據(jù)表用當前數(shù)據(jù)刷新后,datagrid在引起并發(fā)性異常的行上顯示一個小error圖標。
用戶的另一個選擇是忽略對下層數(shù)據(jù)庫的更改,強迫他的改變生效。有多種方法可以實現(xiàn)該功能。第一種是sql server數(shù)據(jù)庫上直接執(zhí)行一個sql語句來使數(shù)據(jù)表中的數(shù)據(jù)同步。盡管這種方法可以實現(xiàn),但是它要求你在數(shù)據(jù)庫更改時為重寫所有的sql語句。使用這種技術編寫的sql是使用的特定數(shù)據(jù)庫版本的硬編碼(hard-coded),丟失了數(shù)據(jù)適配器和數(shù)據(jù)集對象提供的抽象性。
更好的選擇是使用我前面所寫的updaterow程序并且允許數(shù)據(jù)適配器處理更新。圖9中的代碼首先建立了含有新的更改的數(shù)據(jù)行的拷貝,接著調用updaterow程序從數(shù)據(jù)庫中獲得新數(shù)據(jù)。調用updaterow過程是必要的,這樣你在試圖再次執(zhí)行更新時才不會接收到并發(fā)性異常。該代碼接著迭代數(shù)據(jù)行中所有的列,使用用戶提供的值更新最近檢索到的值。最后,使用數(shù)據(jù)適配器更新下層數(shù)據(jù)庫。
這些代碼解決方法都有一些潛在的問題。首先,默認情況下數(shù)據(jù)適配器的update方法在第一個并發(fā)性異常時就會失敗,不處理后面的數(shù)據(jù)行。這會導致數(shù)據(jù)的部分更新或者幾個并發(fā)性異常,每一個由用戶單獨處理。圖7中的代碼顯示了另一個潛在的異常,在強迫用戶的更改到下層數(shù)據(jù)庫的時候。有很小的機會出現(xiàn)另一個用戶在調用updaterow程序和執(zhí)行dacustomer的update方法之間更改了下層數(shù)據(jù)庫。這將產(chǎn)生一個未處理的dbconcurrencyexception。一種可能的解決方法是把update方法放入try...catch塊,但是第二個catch代碼可能與第一個相似,并且能潛在的產(chǎn)生它自己的并發(fā)性異常,就需要更多的try...catch塊,直到無窮。更好的解決方法是將這段代碼放入一個獨立的在多個并發(fā)性異常發(fā)生的情況下可以調用的程序中。
使用sql server觸發(fā)器
為該公司開發(fā)顧客服務應用程序的人員已經(jīng)處理好joe更新顧客信息時接收到的dbconcurrencyexception異常的代碼,應用程序工作又得很好了--直到下午sally試圖輸入一個訂單為止。
sally接到一個顧客的訂單電話。該顧客信息已經(jīng)在數(shù)據(jù)庫中了,因此sally使用訂單的基本信息包括郵寄地址。接著她打開orderdetails屏幕并給訂單添加了兩個項目。每個orderdetail記錄包括orderid 、productid 、quantity 和price。訂單填完后,sally點擊place order按鈕把訂單插入數(shù)據(jù)庫。我通過為orderdetail記錄添加硬編碼值簡化了代碼(如圖3所示)。
訂單成功地插入和數(shù)據(jù)庫,但是sally接到一個提示,顯示一個項沒有貨了,訂單不能在兩周內發(fā)貨。該顧客意識到包裹送達時他不在城里,就詢問是否可以更改發(fā)貨地址。sally在使用order maintenance屏幕前已經(jīng)更改了訂單信息,因此她說樂于幫忙。她打開適當?shù)钠聊徊樵擃櫩透淖儼l(fā)貨地址。但是她點擊save changes按鈕時,看到了一個與joe相似的錯誤對話框,應用程序崩潰了。
理解為什么產(chǎn)生dbconcurrencyexception的關鍵在于下層數(shù)據(jù)庫。回想圖5中我在orderdetail表上設置了一個觸發(fā)器,所有的inserts、updates和deletes都將調用它。trg_updateordertotal通過計算order中每個orderdetail行的數(shù)量乘以價格更新訂單的總價格。當sally建立一個有兩個項的訂單時,應用程序首先向orders中插入一個新行,接著向orderdetail插入兩個新行。在每個orderdetail記錄建立后,調用觸發(fā)器更新order記錄的total列。但是這個值只在數(shù)據(jù)庫中產(chǎn)生并且不會傳遞到應用程序。實際上,圖3中的代碼沒有指定total,因為在圖1的代碼中已經(jīng)指定了一個默認值。接著默認值0隨著新的orders記錄傳遞進sql server,該值在訂單建立時是正確的。數(shù)據(jù)庫接著更新total列,但是應用程序中的數(shù)據(jù)表仍然使用0作為訂單總價。當sally試圖更新order記錄時,數(shù)據(jù)適配器認為total列的值已經(jīng)改變了并產(chǎn)生一個dbconcurrencyexception異常。
這兒的陷阱是過去當sally已經(jīng)更新order記錄時,碰巧自從最后一次數(shù)據(jù)自動刷新發(fā)生后,她沒有更新剛才建立的新訂單。在建立每個訂單30分鐘內,dsalldata數(shù)據(jù)集對象被刷新,它將使用正確的合計值更新orders數(shù)據(jù)表。任何在數(shù)據(jù)自動更新(或者應用程序重新啟動)后作的更新將按預計的情形工作。
這個問題的解決方法與前面的問題相似:刷新用戶看到的數(shù)據(jù)。但是我能夠更主動,不是等待并發(fā)性異常發(fā)生。我能在建立orderdetail記錄后,自動刷新每一個orders數(shù)據(jù)行。我能更改圖3中的代碼,使它包含一個updaterow程序的調用,在調用daorderdetail的update方法后指定orders中的行。sql server將正常完成該觸發(fā)器,但是你不能依賴它,因此開發(fā)者也許會添加一個時間延遲,這樣觸發(fā)器有足夠的時間完成。
結論
ado.net數(shù)據(jù)集對象的不連接特性提供了很高的性能,也給應用程序帶來了數(shù)據(jù)并發(fā)性類型的錯誤。了解這些錯誤怎樣和為什么出現(xiàn)將允許你很好地捕捉和處理這種錯誤,給應用程序增加另一個層次的支持。這給用戶維護下層數(shù)據(jù)的完整性更多的選擇。伴隨著應用程序進一步遷移到互聯(lián)網(wǎng)和內部網(wǎng),用戶成倍增加,并發(fā)性問題的處理面臨更大的挑戰(zhàn)。ado.net為解決該問題提供了所有的必要工具。
新聞熱點
疑難解答