Scott Mitchell 的ASP.NET 2.0数据教程之二十一:: 实现开放式并发
在ASP.NET 2.0中操作數(shù)據(jù):實(shí)現(xiàn)開放式并發(fā)
?
下載本教程中的編碼例子 | 下載本教程的PDF版
導(dǎo)言
對(duì)于那些僅僅允許用戶查看數(shù)據(jù),或者僅有一個(gè)用戶可以修改數(shù)據(jù)的web應(yīng)用軟件,不存在多用戶并發(fā)沖突的問(wèn)題。然而對(duì)于那些允許多個(gè)用戶修改或刪除數(shù)據(jù)的web應(yīng)用軟件,則有可能發(fā)生一個(gè)用戶所做的更改與另一個(gè)并發(fā)用戶的更改沖突。在沒有任何并發(fā)策略的地方,當(dāng)兩個(gè)用戶同時(shí)編輯某一條記錄,最后提交的用戶的更改將覆蓋先提交的用戶所作的更改。
?
例如,假設(shè)兩個(gè)用戶,Jisun和Sam,都訪問(wèn)我們的應(yīng)用軟件中的一個(gè)頁(yè)面,這個(gè)頁(yè)面允許訪問(wèn)者通過(guò)一個(gè)GridView控件更新和刪除產(chǎn)品數(shù)據(jù)。他們都同時(shí)點(diǎn)擊GridView控件中的Edit按鈕。Jisun把產(chǎn)品名稱更改為“Chai Tea”并點(diǎn)擊Update按鈕,實(shí)質(zhì)結(jié)果是向數(shù)據(jù)庫(kù)發(fā)送一個(gè)UPDATE語(yǔ)句,它將更新此產(chǎn)品的所有可修改的字段(盡管Jisun實(shí)際上只修改了一個(gè)字段:ProductName)。在這一刻,數(shù)據(jù)庫(kù)中包含有這條產(chǎn)品記錄“Chai Tea”—種類為Beverages、供應(yīng)商為Exotic Liquids、等該產(chǎn)品的詳細(xì)信息。然而,在Sam的屏幕中的GridView里,當(dāng)前編輯行里顯示的產(chǎn)片名稱依舊是“Chai”。在Jisun的更改被提交后片刻,Sam把種類更改為“Condiments”并點(diǎn)擊Update按鈕。這個(gè)發(fā)送到數(shù)據(jù)庫(kù)的UPDATE語(yǔ)句的結(jié)果是將產(chǎn)品名稱更改為“Chai”、CategoryID字段的值是種類Beverages對(duì)應(yīng)的ID,等等。Jisun所作的對(duì)產(chǎn)品名稱的更改就被覆蓋了。圖1展示了這些連續(xù)的事件。
圖 1: 當(dāng)兩個(gè)用戶同時(shí)更新一條記錄,則存在一個(gè)用戶的更改覆蓋另一個(gè)的更改的可能性
?
類似地,當(dāng)兩個(gè)用戶同時(shí)訪問(wèn)一個(gè)頁(yè)面,一個(gè)用戶可能更新的事另一個(gè)用戶已經(jīng)刪除的記錄。或者,在一個(gè)用戶加載頁(yè)面跟他點(diǎn)擊刪除按鈕之間的時(shí)間里,另一個(gè)用戶修改了這條記錄的內(nèi)容。
有下面三中并發(fā)控制策略可供選擇:
????????? 什么都不做 –如果并發(fā)用戶修改的是同一條記錄,讓最后提交的結(jié)果生效(默認(rèn)的行為)
????????? 開放式并發(fā)(Optimistic Concurrency) - 假定并發(fā)沖突只是偶爾發(fā)生,絕大多數(shù)的時(shí)候并不會(huì)出現(xiàn); 那么,當(dāng)發(fā)生一個(gè)沖突時(shí),僅僅簡(jiǎn)單的告知用戶,他所作的更改不能保存,因?yàn)閯e的用戶已經(jīng)修改了同一條記錄
????????? 保守式并發(fā)(Pessimistic Concurrency) – 假定并發(fā)沖突經(jīng)常發(fā)生,并且用戶不能容忍被告知自己的修改不能保存是由于別人的并發(fā)行為;那么,當(dāng)一個(gè)用戶開始編輯一條記錄,鎖定該記錄,從而防止其他用戶編輯或刪除該記錄,直到他完成并提交自己的更改
?
注意:在本節(jié)里,我們不討論保守式并附的例子。保守式并發(fā)控制很少使用,因?yàn)殒i定如果沒有完全釋放,會(huì)妨礙其他用戶進(jìn)行數(shù)據(jù)更新。例如,如果一個(gè)用戶為了編輯而鎖定某一條記錄,但在解鎖之前就離開了,那么其他任何用戶都不能更新這條記錄,直到最初的用戶返回并完成他的更新。因此,使用保守式并發(fā)控制的地方,相應(yīng)地會(huì)作一個(gè)時(shí)間限制,如果到達(dá)這個(gè)時(shí)間限制,則取消鎖定。例如訂票網(wǎng)站,當(dāng)用戶完成他的訂票過(guò)程時(shí)會(huì)鎖定某個(gè)特定的座位,這就是一個(gè)使用保守式并發(fā)控制的例子。
?
第一步:如何實(shí)現(xiàn)開放式并發(fā)控制
開放式并發(fā)控制能夠確保一條記錄在更新或者刪除時(shí)跟它開始這次更新或修改過(guò)程時(shí)保持一致。例如,當(dāng)在一個(gè)可編輯的GridView里點(diǎn)擊編輯按鈕時(shí),該記錄的原始值從數(shù)據(jù)庫(kù)中讀取出來(lái)并顯示在TextBox和其他Web控件中。這些原始的值保存在GridView里。隨后,當(dāng)用戶完成他的修改并點(diǎn)擊更新按鈕,這些原始值加上修改后的新值發(fā)送到業(yè)務(wù)邏輯層,然后到數(shù)據(jù)訪問(wèn)層。數(shù)據(jù)訪問(wèn)層必定發(fā)出一個(gè)SQL語(yǔ)句,它將僅僅更新那些開始編輯時(shí)的原始值根數(shù)據(jù)庫(kù)中的值一致的記錄。圖二描述了這些事件發(fā)生的順序。
圖2: 為了更新或刪除能夠成功,原始值必須與數(shù)據(jù)庫(kù)中相應(yīng)的值一致
?
有多種方法可以實(shí)現(xiàn)開放式并發(fā)控制(查看Peter A. Bromberg的文章 ?Optmistic Concurrency Updating Logic,從摘要中看到許多選擇)。ADO.NET類型化數(shù)據(jù)集提供了一種應(yīng)用,這只需要在配置時(shí)勾選上一個(gè)CheckBox。使用開發(fā)式并發(fā)的目的是使類型化數(shù)據(jù)集的TableAdapter的UPDATE和DELETE語(yǔ)句可以檢測(cè)自該記錄加載到DataSet中以來(lái)數(shù)據(jù)庫(kù)中的值是否被更改。例如下面的UPDATE語(yǔ)句,當(dāng)當(dāng)前數(shù)據(jù)庫(kù)中的值與GridView中開始編輯的原始值一致才更新某個(gè)產(chǎn)品的名稱和價(jià)格。@ProductName 和 @UnitPrice參數(shù)包含的是用戶輸入的新值,而參數(shù)@original_ProductName 和 @original_UnitPrice則包含最初點(diǎn)擊編輯按鈕時(shí)加載到GridView中的值:
?
1UPDATE?Products?SET2????ProductName?=?@ProductName,
3????UnitPrice?=?@UnitPrice
4WHERE
5????ProductID?=?@original_ProductID?AND
6????ProductName?=?@original_ProductName?AND
7????UnitPrice?=?@original_UnitPrice
8
?
注意:這個(gè)UPDATE語(yǔ)句是為了易讀而簡(jiǎn)單化了。實(shí)際上,在WHERE子句中檢測(cè)UnitPrice會(huì)比較棘手,這是因?yàn)?/span>UnitPrice可以包含空值,而NULL = NULL則總是返回False(相應(yīng)地你必須用IS NULL)。
?
除了使用一個(gè)不同的UPDATE語(yǔ)句之外,配置TableAdapter使用開放式并發(fā)控制還需要修改它直接發(fā)送到數(shù)據(jù)庫(kù)的方法。回到我們的第一節(jié),創(chuàng)建一個(gè)數(shù)據(jù)訪問(wèn)層,這些發(fā)送到數(shù)據(jù)庫(kù)的方法接收一列標(biāo)量的值作為輸入?yún)?shù)(不僅僅是強(qiáng)類型DataRow或DataTable的實(shí)例)。當(dāng)使用開放式并發(fā),直接發(fā)送到數(shù)據(jù)庫(kù)的Update() 和 Delete()方法就包含了對(duì)應(yīng)原始值的輸入?yún)?shù)。而且,業(yè)務(wù)邏輯層中批量方式更新的代碼(Update()的重載,它不僅接受標(biāo)量值,也接受DataRows 和 DataTables)也要做出相應(yīng)的更改。
與其擴(kuò)展我們現(xiàn)有得數(shù)據(jù)訪問(wèn)層表適配器使用開放式并發(fā)(同時(shí)也必須修改業(yè)務(wù)邏輯層以協(xié)調(diào)),不如讓我們創(chuàng)建一個(gè)新的類型化數(shù)據(jù)集NorthwindOptimisticConcurrency,在它里面我們添加一個(gè)使用開放式并發(fā)的Products表適配器。然后,我們將在業(yè)務(wù)邏輯層中創(chuàng)建類ProductsOptimisticConcurrencyBLL,它為了支持開放式并發(fā)的DAL會(huì)有適當(dāng)?shù)母摹R坏┻@些基礎(chǔ)工作都已完成,我們就可以創(chuàng)建ASP.NET頁(yè)面。
?
第二步: 創(chuàng)建一個(gè)支持開放式并發(fā)的數(shù)據(jù)訪問(wèn)層
為了創(chuàng)建一個(gè)新的類型化數(shù)據(jù)集,在App_Code文件夾里的DAL文件夾上右鍵點(diǎn)擊,選擇添加一個(gè)新的數(shù)據(jù)集并命名為NorthwindOptimisticConcurrency。正如我們?cè)诘谝还?jié)中看到過(guò)的那樣,系統(tǒng)會(huì)自動(dòng)添加一個(gè)表適配器(TableAdapter)到當(dāng)前的類型化數(shù)據(jù)集眾,并自動(dòng)地進(jìn)入TableAdapter配置向?qū)АT诘谝黄林?#xff0c;向?qū)崾疚覀冞x擇數(shù)據(jù)庫(kù)連接 – 連接到同樣的數(shù)據(jù)庫(kù)Northwind并使用Web.config里設(shè)置好的連接字符串NORTHWNDConnectionString。
圖 3: 連接到同一個(gè)數(shù)據(jù)庫(kù)Northwind
下一步,向?qū)崾疚覀冞x擇如何訪問(wèn)數(shù)據(jù)庫(kù):通過(guò)一個(gè)指定的SQL語(yǔ)句,創(chuàng)建新的存儲(chǔ)過(guò)程,或者使用一個(gè)現(xiàn)有的存儲(chǔ)過(guò)程。既然我們最初的DAL是使用的是指定SQL查詢語(yǔ)句,這里我們還是使用它。
圖4: 使用指定SQL語(yǔ)句的方式訪問(wèn)數(shù)據(jù)庫(kù)
下一步,進(jìn)入查詢分析器,返回產(chǎn)品信息。讓我們使用在最初的DAL中產(chǎn)品TableAdapter相同的SQL查詢,它返回產(chǎn)品的所有字段包括產(chǎn)品的供應(yīng)商和類別名稱。
?
?1SELECT???ProductID,?ProductName,?SupplierID,?CategoryID,?QuantityPerUnit,?2
?3???????????UnitPrice,?UnitsInStock,?UnitsOnOrder,?ReorderLevel,?Discontinued,
?4
?5???????????(SELECT?CategoryName?FROM?Categories?WHERE?Categories.CategoryID?=?Products.CategoryID)?as?CategoryName,?
?6
?7???????????(SELECT?CompanyName?FROM?Suppliers?WHERE?Suppliers.SupplierID?=?Products.SupplierID)?as?SupplierName
?8
?9FROM???????Products?
10
圖5:使用在最初的DAL中產(chǎn)品TableAdapter相同的SQL查詢
?
在我們進(jìn)入下一步之前,點(diǎn)擊“高級(jí)選項(xiàng)”按鈕。要讓這個(gè)TableAdapter使用開放式并發(fā),僅僅需要勾選上“使用開放式并發(fā)”。
圖6:勾選“使用開放式并發(fā)”啟用開放式并發(fā)控制
最后,需要指出的是,該TableAdapter應(yīng)該同時(shí)使用“填充DataTable”和“返回DataTable”兩種要生成的方法;并且,勾選“創(chuàng)建方法以將更新直接發(fā)送到數(shù)據(jù)庫(kù)(GenerateDBDirectMethods)”。將返回DataTable的方法名稱從GetData改為GetProducts,使之與我們最初的DAL中的命名規(guī)則匹配。
圖7:讓這個(gè)TableAdapter利用所有的數(shù)據(jù)訪問(wèn)方式
完成了配置向?qū)Ш?#xff0c;該數(shù)據(jù)集設(shè)計(jì)器將包含一個(gè)強(qiáng)類型的Products DataTable和TableAdapter。讓我們花些時(shí)間把該DataTable的名稱Products改為ProductsOptimisticConcurrency,方法是右鍵點(diǎn)擊DataTable的標(biāo)題欄,從菜單中選擇“重命名”。
圖8:一個(gè)DataTable和TableAdapter已經(jīng)添加到類型化數(shù)據(jù)集
為了看看ProductsOptimisticConcurrency TableAdapter(使用開放式并發(fā))和Products TableAdapter(不使用并發(fā)控制)的UPDATE 和 DELETE查詢之間有什么不同,選中該TableAdapter并轉(zhuǎn)到屬性窗口。在DeleteCommand 和 UpdateCommand 這兩個(gè)屬性的 CommandText 子屬性里,我們可以看到調(diào)用DAL的update或者delete關(guān)聯(lián)的方法時(shí)發(fā)送到數(shù)據(jù)庫(kù)的實(shí)際的SQL語(yǔ)法。ProductsOptimisticConcurrency TableAdapter使用的DELETE語(yǔ)句是:
?
DELETE?FROM?[Products]?????WHERE?(([ProductID]?=?@Original_ProductID)?
????AND?([ProductName]?=?@Original_ProductName)?
????AND?((@IsNull_SupplierID?=?1?AND?[SupplierID]?IS?NULL)?OR?([SupplierID]?=?@Original_SupplierID))?
????AND?((@IsNull_CategoryID?=?1?AND?[CategoryID]?IS?NULL)?OR?([CategoryID]?=?@Original_CategoryID))?
????AND?((@IsNull_QuantityPerUnit?=?1?AND?[QuantityPerUnit]?IS?NULL)?OR?([QuantityPerUnit]?=?@Original_QuantityPerUnit))?
????AND?((@IsNull_UnitPrice?=?1?AND?[UnitPrice]?IS?NULL)?OR?([UnitPrice]?=?@Original_UnitPrice))?
????AND?((@IsNull_UnitsInStock?=?1?AND?[UnitsInStock]?IS?NULL)?OR?([UnitsInStock]?=?@Original_UnitsInStock))?
????AND?((@IsNull_UnitsOnOrder?=?1?AND?[UnitsOnOrder]?IS?NULL)?OR?([UnitsOnOrder]?=?@Original_UnitsOnOrder))?
????AND?((@IsNull_ReorderLevel?=?1?AND?[ReorderLevel]?IS?NULL)?OR?([ReorderLevel]?=?@Original_ReorderLevel))?
????AND?([Discontinued]?=?@Original_Discontinued))?
?
相反,最初的DAL的Products TableAdapter所使用的DELETE語(yǔ)句則簡(jiǎn)單得多:
?
DELETE?FROM?[Products]?WHERE?(([ProductID]?=?@Original_ProductID))?
?
正如你所看到的,啟用了開發(fā)式并發(fā)的TableAdapter所使用的DELETE語(yǔ)句里的WHERE子句包含了對(duì)表Product每一個(gè)字段現(xiàn)有的值與GridView(或者DetailsView,FormView)最后一次加載時(shí)的原始值的對(duì)比。因?yàn)槌?/span>ProductID,ProductName, 和Discontinued之外,其他所有字段都可能為NULL值,所以WHERE子句里還包含了額外的參數(shù)以及與NULL值恰當(dāng)?shù)谋容^。
在這一節(jié)里,我們不會(huì)在啟用了開放式并發(fā)的數(shù)據(jù)集里增加其他的DataTable了,因?yàn)槲覀兊?/span>ASP.NET頁(yè)面將僅提供更新和刪除產(chǎn)品信息的功能。然而,我們?nèi)匀恍枰?/span>ProductsOptimisticConcurrency TableAdapter里添加GetProductByProductID(productID) 方法。
為了實(shí)現(xiàn)這一點(diǎn),在TableAdapter的標(biāo)題欄(在Fill和GetProducts方法名的上方)上右鍵并從菜單里選擇“添加查詢”。這將啟動(dòng)TableAdapter查詢配置向?qū)?。?/span>TableAdapter的最初配置的基礎(chǔ)上,選擇指定SQL語(yǔ)句來(lái)創(chuàng)建GetProductByProductID(productID)方法(見圖四)。因?yàn)?/span>GetProductByProductID(productID)方法返回指定產(chǎn)品的信息,因此需要指定SQL查詢類型為“SELECT(返回行)”。
圖9:標(biāo)記SQL查詢類型為“SELECT(返回行)”
進(jìn)入下一步,向?qū)崾疚覀冎付?/span>SQL語(yǔ)句,并且與載入TableAdapter默認(rèn)查詢語(yǔ)句。在現(xiàn)有的查詢語(yǔ)句的基礎(chǔ)上添加WHERE ProductID = @ProductID子句,如圖10:
圖10:在預(yù)載入的查詢語(yǔ)句上添加WHERE子句從而返回特定的產(chǎn)品記錄
最后,把生成的方法重命名為FillByProductID和GetProductByProductID。
圖11:把生成的方法重命名為FillByProductID和GetProductByProductID
完成這個(gè)向?qū)е?#xff0c;現(xiàn)在這個(gè)TableAdapter包含兩個(gè)訪問(wèn)數(shù)據(jù)的方法:GetProducts(),它返回所有 的產(chǎn)品;和GetProductByProductID(productID),它返回特定的產(chǎn)品。
?
第三步: 創(chuàng)建一個(gè)支持啟用了開放式并發(fā)的DAL的業(yè)務(wù)邏輯層
我們現(xiàn)有的ProductsBLL類包含批量更新和直接發(fā)送數(shù)據(jù)庫(kù)的模式的例子。AddProduct方法和 UpdateProduct重載都使用了批量更新模式,通過(guò)一個(gè)ProductRow實(shí)例發(fā)送到TableAdapter的Update方法。另一方面,DeleteProduct方法則使用直接發(fā)送到數(shù)據(jù)庫(kù)的模式,調(diào)用TableAdapter的Delete(productID)方法。
在新的ProductsOptimisticConcurrency TableAdapter里,發(fā)送到數(shù)據(jù)庫(kù)的方法現(xiàn)還要求傳入原始的值。例如,Delete方法現(xiàn)在要求十個(gè)輸入?yún)?shù):原始的ProductID、ProductName、SupplierID、CategoryID、QuantityPerUnit、UnitPrice、UnitsInStock、UnitsOnOrder、ReorderLevel和Discontinued。它在發(fā)送到數(shù)據(jù)庫(kù)的DELETE語(yǔ)句的WHERE子句里使用這些額外的輸入?yún)?shù),僅僅刪除當(dāng)前數(shù)據(jù)庫(kù)的值與原始值一致的指定記錄。
使用批量更新模式時(shí),如果標(biāo)記給TableAdapter的Update使用的方法沒有更改,那么代碼就需要同時(shí)記錄原始值和新的值。然而,與其在我們現(xiàn)有的ProductsBLL類的基礎(chǔ)上試圖使用啟用了開放式并發(fā)的DAL,不如讓我們重新創(chuàng)意一個(gè)業(yè)務(wù)邏輯類支持我們新的DAL。
在App_Code文件夾下的BLL子文件夾里,添加一個(gè)名為ProductsOptimisticConcurrencyBLL的新類。
圖 12: 添加ProductsOptimisticConcurrencyBLL類到BLL文件夾
然后,在ProductsOptimisticConcurrencyBLL類里添加如下代碼:
?
?1using?System;?2
?3using?System.Data;
?4
?5using?System.Configuration;
?6
?7using?System.Web;
?8
?9using?System.Web.Security;
10
11using?System.Web.UI;
12
13using?System.Web.UI.WebControls;
14
15using?System.Web.UI.WebControls.WebParts;
16
17using?System.Web.UI.HtmlControls;
18
19using?NorthwindOptimisticConcurrencyTableAdapters;
20
21?
22
23?
24
25[System.ComponentModel.DataObject]
26
27public?class?ProductsOptimisticConcurrencyBLL
28
29{
30
31????private?ProductsOptimisticConcurrencyTableAdapter?_productsAdapter?=?null;
32
33????protected?ProductsOptimisticConcurrencyTableAdapter?Adapter
34
35????{
36
37????????get
38
39????????{
40
41????????????if?(_productsAdapter?==?null)
42
43????????????????_productsAdapter?=?new?ProductsOptimisticConcurrencyTableAdapter();
44
45?
46
47????????????return?_productsAdapter;
48
49????????}
50
51????}
52
53?
54
55????[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select,?true)]
56
57????public?NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable?GetProducts()
58
59????{
60
61????????return?Adapter.GetProducts();
62
63????}
64
65}?
66
?
注意在類的聲明開始之前的using NorthwindOptimisticConcurrencyTableAdapters語(yǔ)句。命名空間NorthwindOptimisticConcurrencyTableAdapters包含了類ProductsOptimisticConcurrencyTableAdapter,它提供DAL的方法。并且,在類聲明之前我們還能找到System.ComponentModel.DataObject屬性標(biāo)志,它指示Visual Studio把該類包含在ObjectDataSource向?qū)У臄?shù)據(jù)對(duì)象下拉列表中。
類ProductsOptimisticConcurrencyBLL的Adapter屬性提供快速訪問(wèn)ProductsOptimisticConcurrencyTableAdapter類的一個(gè)實(shí)例,并和我們最初的BLL類(ProductsBLL、CategoriesBLL等等)相似。最后,方法GetProducts()僅僅是調(diào)用DAL的GetProdcuts()方法并返回一個(gè)ProductsOptimisticConcurrencyDataTable對(duì)象,該對(duì)象由對(duì)應(yīng)數(shù)據(jù)庫(kù)里每一個(gè)產(chǎn)品記錄的ProductsOptimisticConcurrencyRow實(shí)例組成。
?
使用支持開放式并發(fā)的發(fā)送到數(shù)據(jù)庫(kù)的模式刪除一個(gè)產(chǎn)品記錄
當(dāng)使用支持開放式并發(fā)的DAL發(fā)送到數(shù)據(jù)庫(kù)的模式,方法必須傳入新值和原始值。對(duì)刪除來(lái)說(shuō),這沒有新的值,所以僅僅需要傳入原始值。那么,在我們的BLL里,我們必須接收所有原始值所為輸入?yún)?shù)。讓ProductsOptimisticConcurrencyBLL類的DeleteProduct方法使用這個(gè)發(fā)送到數(shù)據(jù)的方法。這意味著此方法必須接受所有的十個(gè)產(chǎn)品數(shù)據(jù)字段作為輸入?yún)?shù),并傳送這些參數(shù)到DAL,如下面的代碼所示:
?1[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Delete,?true)]
?2
?3public?bool?DeleteProduct(int?original_productID,?string?original_productName,?int??original_supplierID,?int??original_categoryID,
?4
?5??????????????????????????string?original_quantityPerUnit,?decimal??original_unitPrice,?short??original_unitsInStock,
?6
?7??????????????????????????short??original_unitsOnOrder,?short??original_reorderLevel,?bool?original_discontinued)
?8
?9{
10
11????int?rowsAffected?=?Adapter.Delete(original_productID,
12
13??????????????????????????????????????original_productName,?
14
15??????????????????????????????????????original_supplierID,
16
17??????????????????????????????????????original_categoryID,
18
19??????????????????????????????????????original_quantityPerUnit,
20
21??????????????????????????????????????original_unitPrice,
22
23??????????????????????????????????????original_unitsInStock,
24
25??????????????????????????????????????original_unitsOnOrder,
26
27??????????????????????????????????????original_reorderLevel,
28
29??????????????????????????????????????original_discontinued);
30
31?
32
33????//?Return?true?if?precisely?one?row?was?deleted,?otherwise?false
34
35????return?rowsAffected?==?1;
36
37}
38
39
?
如果這些在GridView(或者是DetailsView、FormView)最后一次加載時(shí)的原始值跟用戶點(diǎn)擊刪除按鈕時(shí)數(shù)據(jù)庫(kù)中的值不一致, WHERE子句將不能匹配任何數(shù)據(jù)庫(kù)記錄,這就沒有記錄會(huì)受到影響。因此,TableAdapter的Delete方法將返回0并且BLL的DeleteProduct方法返回false。
?
使用支持開放式并發(fā)的批量更新模式修改一個(gè)產(chǎn)品記錄
正如之前注意到的,批量更新模式時(shí)用的TableAdapter的Update方法也有同樣的方法聲明為不管是否支持開放式并發(fā)。也就是,Update方法可以接受一個(gè)DataRow,一批DataRow,一個(gè)DataTable,或者一個(gè)類型化的數(shù)據(jù)集。正是因?yàn)?/span>DataTable在它的DataRow(s)里保留了從原始值到修改后的值這個(gè)變化的軌跡使這成為可能。當(dāng)DAL生成它的UPDATE語(yǔ)句時(shí),參數(shù)@original_ColumnName裝入DataRow中的原始值,反之,參數(shù)@ColumnName裝入DataRow中修改后的值。
在類ProductsBLL(我們最初使用的不支持開放式并發(fā)DAL的)里,當(dāng)我們使用批量更新模式更新產(chǎn)品信息時(shí),我們的代碼執(zhí)行的則是按順序執(zhí)行下列世間:
1.??????? 使用TableAdapter的GetProductByProductID(productID)方法讀取當(dāng)前數(shù)據(jù)庫(kù)中的產(chǎn)品信息到ProductRow實(shí)例
2.??????? 在第1步里將新的值賦值到ProductRow實(shí)例
3.??????? 調(diào)用TableAdapter的Update方法,傳入該ProductRow實(shí)例
這一連串的步驟,無(wú)論如何都不可能支持開放式并發(fā),因?yàn)樵诘谝徊街挟a(chǎn)生的ProductRow是直接從數(shù)據(jù)庫(kù)組裝的,這意味著,DataRow中使用的原始值是當(dāng)前存在于數(shù)據(jù)庫(kù)中值,而并非開始編輯過(guò)程時(shí)綁定到GridView的值。相反地,當(dāng)使用啟用開放式并發(fā)的DAL,我們需要修改UpdateProduct方法的重載以使用下面這些步驟:
1.??????? 使用TableAdapter的GetProductByProductID(productID)方法讀取當(dāng)前數(shù)據(jù)庫(kù)中的產(chǎn)品信息到ProductsOptimisticConcurrencyRow實(shí)例
2.??????? 在第1步里將原始 值賦值到ProductsOptimisticConcurrencyRow實(shí)例
3.??????? 調(diào)用ProductsOptimisticConcurrencyRow實(shí)例的AcceptChanges()方法,這指示DataRow目前這些值是“原始”的值
4.??????? 將新 的值賦值到ProductsOptimisticConcurrencyRow實(shí)例
5.??????? 調(diào)用TableAdapter的Update方法,傳入該ProductsOptimisticConcurrencyRow實(shí)例
第1步讀取當(dāng)前數(shù)據(jù)庫(kù)里指定產(chǎn)品記錄的所有字段的值。對(duì)更新所有 產(chǎn)品字段的UpdateProduct的重載里,這一步是多余的(因?yàn)檫@些值在第2步中被改寫),而對(duì)那些僅僅傳入部分字段值的重載方法來(lái)說(shuō)則是必要的。一旦原始值賦值到ProductsOptimisticConcurrencyRow實(shí)例,調(diào)用AcceptChanges()方法,這將當(dāng)前DataRow中的值標(biāo)記為原始值,這些值將用作UPDATE語(yǔ)句的@original_ColumnNam參數(shù)。然后,新的參數(shù)值被賦值到ProductsOptimisticConcurrencyRow,最后,調(diào)用Update方法,傳入這個(gè)DataRow。
下面這些代碼展示了重載方法UpdateProduct接受所有產(chǎn)品數(shù)據(jù)字段作為輸入?yún)?shù)。雖然這里沒有展示,實(shí)際上從本節(jié)教程下載的ProductsOptimisticConcurrencyBLL類里還包含了重載方法UpdateProduct,它僅僅接受產(chǎn)品名稱和單價(jià)作為輸入?yún)?shù)。
?1protected?void?AssignAllProductValues(NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow?product,
?2??????????????????????????????????????string?productName,?int??supplierID,?int??categoryID,?string?quantityPerUnit,
?3??????????????????????????????????????decimal??unitPrice,?short??unitsInStock,?short??unitsOnOrder,?short??reorderLevel,
?4??????????????????????????????????????bool?discontinued)
?5{
?6????product.ProductName?=?productName;
?7????if?(supplierID?==?null)?product.SetSupplierIDNull();?else?product.SupplierID?=?supplierID.Value;
?8????if?(categoryID?==?null)?product.SetCategoryIDNull();?else?product.CategoryID?=?categoryID.Value;
?9????if?(quantityPerUnit?==?null)?product.SetQuantityPerUnitNull();?else?product.QuantityPerUnit?=?quantityPerUnit;
10????if?(unitPrice?==?null)?product.SetUnitPriceNull();?else?product.UnitPrice?=?unitPrice.Value;
11????if?(unitsInStock?==?null)?product.SetUnitsInStockNull();?else?product.UnitsInStock?=?unitsInStock.Value;
12????if?(unitsOnOrder?==?null)?product.SetUnitsOnOrderNull();?else?product.UnitsOnOrder?=?unitsOnOrder.Value;
13????if?(reorderLevel?==?null)?product.SetReorderLevelNull();?else?product.ReorderLevel?=?reorderLevel.Value;
14????product.Discontinued?=?discontinued;
15}
16
17[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update,?true)]
18public?bool?UpdateProduct(
19??????????????????????????//?new?parameter?values
20??????????????????????????string?productName,?int??supplierID,?int??categoryID,?string?quantityPerUnit,
21??????????????????????????decimal??unitPrice,?short??unitsInStock,?short??unitsOnOrder,?short??reorderLevel,
22??????????????????????????bool?discontinued,?int?productID,
23
24??????????????????????????//?original?parameter?values
25??????????????????????????string?original_productName,?int??original_supplierID,?int??original_categoryID,
26??????????????????????????string?original_quantityPerUnit,?decimal??original_unitPrice,?short??original_unitsInStock,
27??????????????????????????short??original_unitsOnOrder,?short??original_reorderLevel,?bool?original_discontinued,
28??????????????????????????int?original_productID)
29{
30????//?STEP?1:?Read?in?the?current?database?product?information
31????NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable?products?=?Adapter.GetProductByProductID(original_productID);
32????if?(products.Count?==?0)
33????????//?no?matching?record?found,?return?false
34????????return?false;
35
36????NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow?product?=?products[0];
37
38????//?STEP?2:?Assign?the?original?values?to?the?product?instance
39????AssignAllProductValues(product,?original_productName,?original_supplierID,
40???????????????????????????original_categoryID,?original_quantityPerUnit,?original_unitPrice,
41???????????????????????????original_unitsInStock,?original_unitsOnOrder,?original_reorderLevel,
42???????????????????????????original_discontinued);
43
44????//?STEP?3:?Accept?the?changes
45????product.AcceptChanges();
46
47????//?STEP?4:?Assign?the?new?values?to?the?product?instance
48????AssignAllProductValues(product,?productName,?supplierID,?categoryID,?quantityPerUnit,?unitPrice,
49???????????????????????????unitsInStock,?unitsOnOrder,?reorderLevel,?discontinued);
50????
51????//?STEP?5:?Update?the?product?record
52????int?rowsAffected?=?Adapter.Update(product);
53
54????//?Return?true?if?precisely?one?row?was?updated,?otherwise?false
55????return?rowsAffected?==?1;
56}
57
?
第四步: 從ASP.NET頁(yè)面把原始值和新值傳入BLL 方法
完成了DAL和BLL后,剩下的工作就是創(chuàng)建一個(gè)能利用系統(tǒng)中內(nèi)建的開放式并發(fā)邏輯的ASP.NET頁(yè)面。特別地,數(shù)據(jù) Web 服務(wù)器控件(GridView,DetailsView或FormView)必須記住它的原始值,并且ObjectDataSource必須同時(shí)傳送這兩套值到業(yè)務(wù)邏輯層。此外,ASP.NET頁(yè)面必須加以配置從而適當(dāng)?shù)靥幚聿l(fā)沖突。
首先,打開EditInsertDelete文件夾中的OptimisticConcurrency.aspx頁(yè)面,添加一個(gè)GridView控件到設(shè)計(jì)器,設(shè)置它的ID屬性為ProductsGrid。從GridView的職能標(biāo)記里,選擇創(chuàng)建一個(gè)新的ObjectDataSource名為ProductsOptimisticConcurrencyDataSource。既然我們希望這個(gè)ObjectDataSource使用支持開放式并發(fā)的DAL,就把它配置為使用ProductsOptimisticConcurrencyBLL對(duì)象。
圖 13: 該ObjectDataSource使用ProductsOptimisticConcurrencyBLL對(duì)象
在向?qū)е袕南吕斜磉x擇GetProducts,UpdateProduct,和DeleteProduct方法。對(duì)UpdateProduct方法,則使用接受所有產(chǎn)品數(shù)據(jù)字段的重載。
?
配置ObjectDataSource控件的屬性
完成了向?qū)е?#xff0c;該ObjectDataSource的聲明標(biāo)記應(yīng)該如下:
?1<asp:ObjectDataSource?ID="ProductsOptimisticConcurrencyDataSource"?runat="server"?DeleteMethod="DeleteProduct"
?2????OldValuesParameterFormatString="original_{0}"?SelectMethod="GetProducts"?TypeName="ProductsOptimisticConcurrencyBLL"
?3????UpdateMethod="UpdateProduct">
?4????<DeleteParameters>
?5????????<asp:Parameter?Name="original_productID"?Type="Int32"?/>
?6????????<asp:Parameter?Name="original_productName"?Type="String"?/>
?7????????<asp:Parameter?Name="original_supplierID"?Type="Int32"?/>
?8????????<asp:Parameter?Name="original_categoryID"?Type="Int32"?/>
?9????????<asp:Parameter?Name="original_quantityPerUnit"?Type="String"?/>
10????????<asp:Parameter?Name="original_unitPrice"?Type="Decimal"?/>
11????????<asp:Parameter?Name="original_unitsInStock"?Type="Int16"?/>
12????????<asp:Parameter?Name="original_unitsOnOrder"?Type="Int16"?/>
13????????<asp:Parameter?Name="original_reorderLevel"?Type="Int16"?/>
14????????<asp:Parameter?Name="original_discontinued"?Type="Boolean"?/>
15????</DeleteParameters>
16????<UpdateParameters>
17????????<asp:Parameter?Name="productName"?Type="String"?/>
18????????<asp:Parameter?Name="supplierID"?Type="Int32"?/>
19????????<asp:Parameter?Name="categoryID"?Type="Int32"?/>
20????????<asp:Parameter?Name="quantityPerUnit"?Type="String"?/>
21????????<asp:Parameter?Name="unitPrice"?Type="Decimal"?/>
22????????<asp:Parameter?Name="unitsInStock"?Type="Int16"?/>
23????????<asp:Parameter?Name="unitsOnOrder"?Type="Int16"?/>
24????????<asp:Parameter?Name="reorderLevel"?Type="Int16"?/>
25????????<asp:Parameter?Name="discontinued"?Type="Boolean"?/>
26????????<asp:Parameter?Name="productID"?Type="Int32"?/>
27????????<asp:Parameter?Name="original_productName"?Type="String"?/>
28????????<asp:Parameter?Name="original_supplierID"?Type="Int32"?/>
29????????<asp:Parameter?Name="original_categoryID"?Type="Int32"?/>
30????????<asp:Parameter?Name="original_quantityPerUnit"?Type="String"?/>
31????????<asp:Parameter?Name="original_unitPrice"?Type="Decimal"?/>
32????????<asp:Parameter?Name="original_unitsInStock"?Type="Int16"?/>
33????????<asp:Parameter?Name="original_unitsOnOrder"?Type="Int16"?/>
34????????<asp:Parameter?Name="original_reorderLevel"?Type="Int16"?/>
35????????<asp:Parameter?Name="original_discontinued"?Type="Boolean"?/>
36????????<asp:Parameter?Name="original_productID"?Type="Int32"?/>
37????</UpdateParameters>
38</asp:ObjectDataSource>
39
?
正如你所看到的,DeleteParameters集合包含了對(duì)應(yīng)ProductsOptimisticConcurrencyBLL類的DeleteProduct方法的每一個(gè)輸入?yún)?shù)的Parameter實(shí)例。同樣地,UpdateParameters集合也包含了對(duì)應(yīng)UpdateProduct每一個(gè)輸入?yún)?shù)的Parameter實(shí)例。
在先前的那些關(guān)于數(shù)據(jù)修改的教程中,我們?cè)谶@里都會(huì)移除ObjectDataSource的OldValuesParameterFormatString屬性,因?yàn)檫@個(gè)屬性需要BLL方法既要求傳入原始值也要求傳入修改后的值。此外,這個(gè)屬性還需要對(duì)應(yīng)原始值的輸入?yún)?shù)的名稱。既然我們現(xiàn)在要把原始值傳送到BLL,那就不要 刪除這個(gè)屬性。
注意:OldValuesParameterFormatString屬性的值必須映射到BLL里接收原始值的輸入?yún)?shù)的名稱。因?yàn)槲覀儼堰@些參數(shù)命名為original_productName,original_supplierID, 等等,我們可以讓OldValuesParameterFormatString屬性的值依舊是original_{0}。然而如果BLL方法的輸入?yún)?shù)名為的old_productName,old_supplierID等等,那么,你不得不把OldValuesParameterFormatString屬性的值改為old_{0}。
?
為了ObjectDataSource能夠正確地將原始值傳送到BLL方法,還有最后一個(gè)屬性需要設(shè)置。ObjectDataSource有一個(gè) ConflictDetection屬性,它可以設(shè)定為下面的 下面兩個(gè)值之一:
????????? OverwriteChanges – 默認(rèn)值; 不將原始值發(fā)送到BLL方法相應(yīng)的輸入?yún)?shù)
????????? CompareAllValues – 將原始值發(fā)送到BLL方法;當(dāng)使用開放式并發(fā)時(shí)使用這一項(xiàng)
稍花些時(shí)間將ConflictDetection屬性設(shè)置為CompareAllValues。
?
配置GridView的屬性和字段
當(dāng)正確的配置完ObjectDataSource的屬性后,讓我們把注意力放在GridView的設(shè)置上。首先,因?yàn)槲覀兿M?/span>GridView支持編輯和刪除,因此,從GridView的智能標(biāo)記中點(diǎn)擊添加新列,從下拉列表中選擇CommandField并勾選上“刪除”和“編輯/更新”。這將增加一個(gè)CommandField,它的ShowEditButton和ShowDeleteButton屬性都已設(shè)置為true。
當(dāng)綁定ProductsOptimisticConcurrencyDataSource ObjectDataSource,該GridView對(duì)應(yīng)每一個(gè)產(chǎn)品數(shù)據(jù)字段都包含一列。雖然這樣的一個(gè)GridView可以被編輯,但用戶的體驗(yàn)將是不可接受的。這沒有對(duì)數(shù)字欄作格式化處理,也沒有validation控件以確保提供product's name并且unit price、units in stock、units on order、和reorder level的值都是大于零的數(shù)字。
跟我們?cè)谥暗?/span>給編輯和新增界面增加驗(yàn)證控件 ?這一節(jié)里所論述的一樣,用戶界面可以通過(guò)將綁定列(BoundFields)替換為模板列(TemplateFields)實(shí)現(xiàn)自定義。我已經(jīng)通過(guò)以下方式修改了這個(gè)GridView和它的編輯界面:
????????? 刪除ProductID、SupplierName、和CategoryName這幾個(gè)綁定列;
????????? 將ProductName綁定列替換為模板列并添加一個(gè)RequiredFieldValidation控件;
????????? 將CategoryID和SupplierID綁定列替換為模板列,并調(diào)整編輯界面,使用DropDownList而不是TextBox。在這些模板列的ItemTemplates里,顯示CategoryName和SupplierName字段;
????????? 將UnitPrice、UnitsInStock、UnitsOnOrder、和ReorderLevel綁定列替換為模板列并添加CompareValidator控件。
?
因?yàn)槲覀冊(cè)谥暗恼鹿?jié)里已經(jīng)詳細(xì)說(shuō)明了如何完成這些任務(wù),我僅僅把最終的聲明語(yǔ)法列出并把具體執(zhí)行留給讀者作為練習(xí)。
?1<asp:GridView?ID="ProductsGrid"?runat="server"?AutoGenerateColumns="False"?DataKeyNames="ProductID"
?2????DataSourceID="ProductsOptimisticConcurrencyDataSource"?OnRowUpdated="ProductsGrid_RowUpdated">
?3????<Columns>
?4????????<asp:CommandField?ShowDeleteButton="True"?ShowEditButton="True"?/>
?5????????<asp:TemplateField?HeaderText="Product"?SortExpression="ProductName">
?6????????????<EditItemTemplate>
?7????????????????<asp:TextBox?ID="EditProductName"?runat="server"?Text='<%#?Bind("ProductName")?%>'></asp:TextBox>
?8????????????????<asp:RequiredFieldValidator?ID="RequiredFieldValidator1"?runat="server"?ControlToValidate="EditProductName"
?9????????????????????ErrorMessage="You?must?enter?a?product?name.">*</asp:RequiredFieldValidator>
10????????????</EditItemTemplate>
11????????????<ItemTemplate>
12????????????????<asp:Label?ID="Label1"?runat="server"?Text='<%#?Bind("ProductName")?%>'></asp:Label>
13????????????</ItemTemplate>
14????????</asp:TemplateField>
15????????<asp:TemplateField?HeaderText="Category"?SortExpression="CategoryName">
16????????????<EditItemTemplate>
17????????????????<asp:DropDownList?ID="EditCategoryID"?runat="server"?DataSourceID="CategoriesDataSource"?AppendDataBoundItems="true"
18????????????????????DataTextField="CategoryName"?DataValueField="CategoryID"?SelectedValue='<%#?Bind("CategoryID")?%>'>
19????????????????????<asp:ListItem?Value="">(None)</asp:ListItem>
20????????????????</asp:DropDownList><asp:ObjectDataSource?ID="CategoriesDataSource"?runat="server"
21????????????????????OldValuesParameterFormatString="original_{0}"?SelectMethod="GetCategories"?TypeName="CategoriesBLL">
22????????????????</asp:ObjectDataSource>
23????????????</EditItemTemplate>
24????????????<ItemTemplate>
25????????????????<asp:Label?ID="Label2"?runat="server"?Text='<%#?Bind("CategoryName")?%>'></asp:Label>
26????????????</ItemTemplate>
27????????</asp:TemplateField>
28????????<asp:TemplateField?HeaderText="Supplier"?SortExpression="SupplierName">
29????????????<EditItemTemplate>
30????????????????<asp:DropDownList?ID="EditSuppliersID"?runat="server"?DataSourceID="SuppliersDataSource"?AppendDataBoundItems="true"
31????????????????????DataTextField="CompanyName"?DataValueField="SupplierID"?SelectedValue='<%#?Bind("SupplierID")?%>'>
32????????????????????<asp:ListItem?Value="">(None)</asp:ListItem>
33????????????????</asp:DropDownList><asp:ObjectDataSource?ID="SuppliersDataSource"?runat="server"
34????????????????????OldValuesParameterFormatString="original_{0}"?SelectMethod="GetSuppliers"?TypeName="SuppliersBLL">
35????????????????</asp:ObjectDataSource>
36????????????</EditItemTemplate>
37????????????<ItemTemplate>
38????????????????<asp:Label?ID="Label3"?runat="server"?Text='<%#?Bind("SupplierName")?%>'></asp:Label>
39????????????</ItemTemplate>
40????????</asp:TemplateField>
41????????<asp:BoundField?DataField="QuantityPerUnit"?HeaderText="Qty/Unit"?SortExpression="QuantityPerUnit"?/>
42????????<asp:TemplateField?HeaderText="Price"?SortExpression="UnitPrice">
43????????????<EditItemTemplate>
44????????????????<asp:TextBox?ID="EditUnitPrice"?runat="server"?Text='<%#?Bind("UnitPrice",?"{0:N2}")?%>'?Columns="8"></asp:TextBox>
45????????????????<asp:CompareValidator?ID="CompareValidator1"?runat="server"?ControlToValidate="EditUnitPrice"
46????????????????????ErrorMessage="Unit?price?must?be?a?valid?currency?value?without?the?currency?symbol?and?must?have?a?value?greater?than?or?equal?to?zero."
47????????????????????Operator="GreaterThanEqual"?Type="Currency"?ValueToCompare="0">*</asp:CompareValidator>
48????????????</EditItemTemplate>
49????????????<ItemTemplate>
50????????????????<asp:Label?ID="Label4"?runat="server"?Text='<%#?Bind("UnitPrice",?"{0:C}")?%>'></asp:Label>
51????????????</ItemTemplate>
52????????</asp:TemplateField>
53????????<asp:TemplateField?HeaderText="Units?In?Stock"?SortExpression="UnitsInStock">
54????????????<EditItemTemplate>
55????????????????<asp:TextBox?ID="EditUnitsInStock"?runat="server"?Text='<%#?Bind("UnitsInStock")?%>'?Columns="6"></asp:TextBox>
56????????????????<asp:CompareValidator?ID="CompareValidator2"?runat="server"?ControlToValidate="EditUnitsInStock"
57????????????????????ErrorMessage="Units?in?stock?must?be?a?valid?number?greater?than?or?equal?to?zero."
58????????????????????Operator="GreaterThanEqual"?Type="Integer"?ValueToCompare="0">*</asp:CompareValidator>
59????????????</EditItemTemplate>
60????????????<ItemTemplate>
61????????????????<asp:Label?ID="Label5"?runat="server"?Text='<%#?Bind("UnitsInStock",?"{0:N0}")?%>'></asp:Label>
62????????????</ItemTemplate>
63????????</asp:TemplateField>
64????????<asp:TemplateField?HeaderText="Units?On?Order"?SortExpression="UnitsOnOrder">
65????????????<EditItemTemplate>
66????????????????<asp:TextBox?ID="EditUnitsOnOrder"?runat="server"?Text='<%#?Bind("UnitsOnOrder")?%>'?Columns="6"></asp:TextBox>
67????????????????<asp:CompareValidator?ID="CompareValidator3"?runat="server"?ControlToValidate="EditUnitsOnOrder"
68????????????????????ErrorMessage="Units?on?order?must?be?a?valid?numeric?value?greater?than?or?equal?to?zero."
69????????????????????Operator="GreaterThanEqual"?Type="Integer"?ValueToCompare="0">*</asp:CompareValidator>
70????????????</EditItemTemplate>
71????????????<ItemTemplate>
72????????????????<asp:Label?ID="Label6"?runat="server"?Text='<%#?Bind("UnitsOnOrder",?"{0:N0}")?%>'></asp:Label>
73????????????</ItemTemplate>
74????????</asp:TemplateField>
75????????<asp:TemplateField?HeaderText="Reorder?Level"?SortExpression="ReorderLevel">
76????????????<EditItemTemplate>
77????????????????<asp:TextBox?ID="EditReorderLevel"?runat="server"?Text='<%#?Bind("ReorderLevel")?%>'?Columns="6"></asp:TextBox>
78????????????????<asp:CompareValidator?ID="CompareValidator4"?runat="server"?ControlToValidate="EditReorderLevel"
79????????????????????ErrorMessage="Reorder?level?must?be?a?valid?numeric?value?greater?than?or?equal?to?zero."
80????????????????????Operator="GreaterThanEqual"?Type="Integer"?ValueToCompare="0">*</asp:CompareValidator>
81????????????</EditItemTemplate>
82????????????<ItemTemplate>
83????????????????<asp:Label?ID="Label7"?runat="server"?Text='<%#?Bind("ReorderLevel",?"{0:N0}")?%>'></asp:Label>
84????????????</ItemTemplate>
85????????</asp:TemplateField>
86????????<asp:CheckBoxField?DataField="Discontinued"?HeaderText="Discontinued"?SortExpression="Discontinued"?/>
87????</Columns>
88</asp:GridView>
89
?
我們已經(jīng)非常接近于完成一個(gè)完整的例子。然而,還有一些細(xì)節(jié)問(wèn)題需要我們慢慢解決。另外,我們還需要一些界面,當(dāng)發(fā)生并發(fā)沖突時(shí)用來(lái)提示用戶。
注意: 為了讓數(shù)據(jù)Web服務(wù)器控件能夠正確地把原始的值傳送到ObjectDataSource(它隨之將其發(fā)送到BLL),將GirdView的EnableViewState屬性設(shè)置為true(默認(rèn)值)是至關(guān)重要的。如果禁用了視圖狀態(tài),這些原始值在postback的時(shí)候?qū)?huì)丟失。
?
傳送正確的原始值到ObjectDataSource
完成了GridView的配置,還有幾個(gè)問(wèn)題。如果這個(gè)ObjectDataSource的ConflictDetection 屬性設(shè)置為CompareAllValues (正如我們所做的),它會(huì)嘗試復(fù)制GridView的原始值到它的Parameter實(shí)例?;氐綀D2查看這個(gè)過(guò)程的圖解。
特別需要指出的是,這個(gè)GridView的原始值是被指定為雙向綁定的。因此,這些必需的原始值是通過(guò)雙向綁定獲取的,并且它們是規(guī)定為可改變的格式,這一點(diǎn)很重要。
為了看看為什么這一點(diǎn)非常重要,花些時(shí)間通過(guò)瀏覽器訪問(wèn)我們的頁(yè)面。正如所預(yù)料那樣,GridView列出每一個(gè)產(chǎn)品,并且每行最左邊的一列都顯示編輯和刪除按鈕。
圖 14: GridView列出所有的產(chǎn)品信息
如果你點(diǎn)擊任意一行的刪除按鈕,則拋出一個(gè)FormatException異常。
圖 15: 嘗試刪除任意一個(gè)產(chǎn)品導(dǎo)致FormatException異常
當(dāng)ObjectDataSource試圖讀取原始的UnitPrice值引發(fā)了一個(gè)FormatException異常。因?yàn)樵撃0辶袑?/span>UnitPrice的值限制為貨幣格式(<%# Bind("UnitPrice", "{0:C}") %>),它包含一個(gè)貨幣符號(hào),例如$19.95。該FormatException異常發(fā)生在ObjectDataSource試圖將字符產(chǎn)轉(zhuǎn)換成小數(shù)。為了繞過(guò)此問(wèn)題,我們有許多種選擇:
????????? 從模板列里刪除貨幣格式限制。就是說(shuō),取代<%# Bind("UnitPrice", "{0:C}") %>,簡(jiǎn)單地使用<%# Bind("UnitPrice") %>。下方的價(jià)格就是沒有格式化的。
????????? 在模板列中顯示UnitPrice時(shí)格式化為貨幣,但是使用Eval關(guān)鍵字實(shí)現(xiàn)綁定。記得Eval是實(shí)現(xiàn)單向綁定的。我們?nèi)匀恍枰峁?/span>UnitPrice的值作為原始的值,因此在模板列里我們依舊需要一個(gè)雙向綁定的聲明,但這可以放在一個(gè)Visible屬性設(shè)置為false的Label服務(wù)器控件里。在模板列里我們可以使用下面的標(biāo)記:
1<ItemTemplate>2????<asp:Label?ID="DummyUnitPrice"?runat="server"?Text='<%#?Bind("UnitPrice")?%>'?Visible="false"></asp:Label>
3????<asp:Label?ID="Label4"?runat="server"?Text='<%#?Eval("UnitPrice",?"{0:C}")?%>'></asp:Label>
4</ItemTemplate>
?
????????? 從模板列里刪除貨幣格式限制,使用 <%# Bind("UnitPrice") %>。在GridView的RowDataBound事件處理里,編碼訪問(wèn)顯示UnitPrice的值的Label服務(wù)器控件并設(shè)置其Text屬性為格式化的版本。
????????? ?????? 讓UnitPrice保留貨幣格式化。在GridView的RowDeleting事件處理里,將現(xiàn)存的UnitPrice的原始值($19.95)替換為實(shí)際的小數(shù)值(使用Decimal.Parse)。在前面的 在ASP.NET頁(yè)面中處理BLL/DAL異常 這一節(jié)教程里我們也已經(jīng)看過(guò)如何RowUpdating事件處理里實(shí)現(xiàn)類似的功能。
在我的例程里我選擇第二種方法,添加一個(gè)隱藏的Label服務(wù)器控件,并將它的Text屬性雙向綁定到無(wú)格式的UnitPrice值。
解決了這個(gè)問(wèn)題之后,再次點(diǎn)擊任意一個(gè)產(chǎn)品的刪除按鈕。這一次,當(dāng)ObjectDataSource嘗試調(diào)用BLL的UpdateProduct方法時(shí)我們得到一個(gè)InvalidOperationException異常。
圖 16: ObjectDataSource找不到具有它要發(fā)送的輸入?yún)?shù)的方法
仔細(xì)看看異常信息,明顯地ObjectDataSource希望調(diào)用一個(gè)BLL的DeleteProduct方法,此方法包含original_CategoryName和original_SupplierName輸入?yún)?shù)。這是因?yàn)?/span>CategoryID和SupplierID模板列的ItemTemplate當(dāng)前是雙向綁定到CategoryName和SupplierName數(shù)據(jù)字段。作為替換,我們需要包含對(duì)CategoryID和SupplierID數(shù)據(jù)字段的Bind聲明。為了實(shí)現(xiàn)這一點(diǎn),把現(xiàn)有的Bind聲明更改為Eval聲明,并且添加隱藏的Label服務(wù)器控件,這些Label的Text屬性使用雙向綁定的方式綁定到CategoryID和SupplierID數(shù)據(jù)字段,如下所示:
?1<asp:TemplateField?HeaderText="Category"?SortExpression="CategoryName">
?2????<EditItemTemplate>
?3????????
?4????</EditItemTemplate>
?5????<ItemTemplate>
?6????????<asp:Label?ID="DummyCategoryID"?runat="server"?Text='<%#?Bind("CategoryID")?%>'?Visible="False"></asp:Label>
?7????????<asp:Label?ID="Label2"?runat="server"?Text='<%#?Eval("CategoryName")?%>'></asp:Label>
?8????</ItemTemplate>
?9</asp:TemplateField>
10<asp:TemplateField?HeaderText="Supplier"?SortExpression="SupplierName">
11????<EditItemTemplate>
12????????
13????</EditItemTemplate>
14????<ItemTemplate>
15????????<asp:Label?ID="DummySupplierID"?runat="server"?Text='<%#?Bind("SupplierID")?%>'?Visible="False"></asp:Label>
16????????<asp:Label?ID="Label3"?runat="server"?Text='<%#?Eval("SupplierName")?%>'></asp:Label>
17????</ItemTemplate>
18</asp:TemplateField>
19
?
通過(guò)這些更改,現(xiàn)在我們可以成功地刪除和編輯產(chǎn)品信息了!在第五步里,我們將看看如何驗(yàn)證刪除時(shí)發(fā)生并發(fā)沖突。但是現(xiàn)在,花幾分鐘嘗試更新和刪除一些記錄,確認(rèn)在單用戶的情況下更新和刪除能夠正常運(yùn)作。
?
第五步: 測(cè)試開放式并發(fā)支持
為了驗(yàn)證并發(fā)沖突是否能夠被發(fā)現(xiàn)(而不是導(dǎo)致數(shù)據(jù)被盲目改寫),我們需要打開兩個(gè)瀏覽器窗口來(lái)訪問(wèn)這個(gè)頁(yè)面。在兩個(gè)瀏覽窗口里,都點(diǎn)擊產(chǎn)品“Chai”的編輯按鈕。然后,在其中一個(gè)窗口修改其名稱為“Chai Tea”并點(diǎn)擊更新。這個(gè)更新應(yīng)該會(huì)成功并且GridView回到預(yù)編輯狀態(tài),并且該產(chǎn)品的名稱已經(jīng)改為“Chai Tea”。
而在另一個(gè)瀏覽器窗口里,產(chǎn)品名稱域依舊顯示的是“Chai”。在這個(gè)瀏覽器窗口,將UnitPrice的值更新為25.00。如果沒有開放式并發(fā)支持的話,點(diǎn)擊第二個(gè)瀏覽器窗口的更新按鈕將把產(chǎn)品名稱改回“Chai”,從而覆蓋了第一個(gè)瀏覽器窗口里所作的修改。然而現(xiàn)在有了開發(fā)式并發(fā),當(dāng)點(diǎn)擊第二個(gè)窗口中的更新按鈕時(shí)導(dǎo)致了一個(gè)DBConcurrencyException異常。
圖 17: 發(fā)現(xiàn)并發(fā)沖突,拋出一個(gè)DBConcurrencyException異常
這個(gè)DBConcurrencyException異常僅當(dāng)利用DAL的批量更新模式時(shí)會(huì)被拋出。直接發(fā)送到數(shù)據(jù)庫(kù)的模式則不會(huì)引發(fā)異常,它僅僅會(huì)提示沒有行受到影響。為了舉例說(shuō)明這個(gè),兩個(gè)瀏覽器窗口里的GridView都回到預(yù)編輯的狀態(tài)。然后,在第一個(gè)窗口里,點(diǎn)擊編輯按鈕,把產(chǎn)品名稱從“Chai”改為“Chai Tea”并點(diǎn)擊更新。在第二個(gè)窗口里,點(diǎn)擊產(chǎn)品“Chai”的刪除按鈕。
點(diǎn)擊刪除按鈕,頁(yè)面會(huì)傳,GridView調(diào)用ObjectDataSource的Delete()方法,然后ObjectDataSource調(diào)用ProductsOptimisticConcurrencyBLL類的DeleteProduct方法,傳入原始的值。在第二個(gè)瀏覽器窗口里原始的ProductName值是“Chai Tea”,這個(gè)值與當(dāng)前數(shù)據(jù)庫(kù)中相應(yīng)的ProductName值是不一致的。因此,發(fā)送到數(shù)據(jù)庫(kù)的DELETE語(yǔ)句影響0行,因?yàn)閿?shù)據(jù)庫(kù)中沒有記錄能夠滿足WHERE子句。DeleteProduct方法返回false并且ObjectDataSource的數(shù)據(jù)重新綁定到GridView控件。
從最后一個(gè)用戶的觀點(diǎn)來(lái)看,在第二個(gè)瀏覽器窗口里點(diǎn)擊了產(chǎn)品“Chai Tea”的刪除按鈕導(dǎo)致屏幕閃爍,恢復(fù)后該產(chǎn)品依舊在,雖然現(xiàn)在它的名稱是“Chai”(在第一個(gè)瀏覽器窗口里修改了產(chǎn)品名稱)。如果用戶再次點(diǎn)擊刪除按鈕,這次就能成功刪除,因?yàn)?/span>GridView的原始的ProductName值(“Chai”)現(xiàn)在能夠與數(shù)據(jù)庫(kù)中相應(yīng)的值匹配。
在這些例子里,用戶的體驗(yàn)跟理想的狀況還有頗遠(yuǎn)的距離。顯然我們?cè)谑褂门扛履J綍r(shí)不希望用戶看到DBConcurrencyException異常生硬的詳細(xì)信息。并且使用直接發(fā)送到數(shù)據(jù)庫(kù)模式的行為也會(huì)讓用戶有些疑惑,因?yàn)橛脩舨僮魇×说菦]有準(zhǔn)確的提示說(shuō)明為什么。
為了補(bǔ)救這兩個(gè)小問(wèn)題,我們可以在頁(yè)面上放置一個(gè)Label服務(wù)器控件,它用來(lái)提供為什么更新或刪除失敗的說(shuō)明。在批量更新模式,我們可以在GridView的post級(jí)事件處理里判定是否引發(fā)了一個(gè)DBConcurrencyException異常,顯示必要的警告標(biāo)簽。對(duì)于直接發(fā)送到數(shù)據(jù)庫(kù)的方法,我們可以檢測(cè)BLL方法(它對(duì)一行或多行產(chǎn)生影響返回true,否則false)的返回值并顯示必要的提示信息。
?
第六步: 添加提示信息并且在發(fā)生并發(fā)沖突時(shí)顯示
當(dāng)一個(gè)并發(fā)沖突出現(xiàn)時(shí),展現(xiàn)出來(lái)的行為取決于是使用DAL的批量更新還是直接發(fā)送到數(shù)據(jù)庫(kù)的模式。我們這一節(jié)的教程兩種模式都用了,用批量更新模式實(shí)現(xiàn)修改,用直接發(fā)送到數(shù)據(jù)庫(kù)的方式實(shí)現(xiàn)刪除。首先,我們添加兩個(gè)Label服務(wù)器控件到頁(yè)面,它們用來(lái)解釋更新或刪除數(shù)據(jù)時(shí)出現(xiàn)的并發(fā)沖突。設(shè)置Label控件的Visible和EnableViewState屬性為false;這意味一般情況下它們都是隱藏的,除非是那些特別的頁(yè)面訪問(wèn),在那里它們的Visible屬性通過(guò)編碼設(shè)置為true。
?
1<asp:Label?ID="DeleteConflictMessage"?runat="server"?Visible="False"?EnableViewState="False"?CssClass="Warning"2?????Text="The?record?you?attempted?to?delete?has?been?modified?by?another?user?since?you?last?visited?this?page.?
3????????????Your?delete?was?cancelled?to?allow?you?to?review?the?other?user's?changes?and?determine?if?you?want?to?continue?deleting?this?record."?/>
4
5<asp:Label?ID="UpdateConflictMessage"?runat="server"?Visible="False"?EnableViewState="False"?CssClass="Warning"
6?????Text="The?record?you?attempted?to?update?has?been?modified?by?another?user?since?you?started?the?update?process.?
7????????????Your?changes?have?been?replaced?with?the?current?values.?Please?review?the?existing?values?and?make?any?needed?changes."?/>
8?
9
?
在設(shè)置了它們的Visible、EnabledViewState和Text屬性之外,我們還要把CssClass屬性設(shè)置為Warning,這讓標(biāo)簽顯示大的、紅色的、斜體、加粗的字體。這個(gè)CSS Warning 分類是在研究插入、更新和刪除的關(guān)聯(lián)事件 這一節(jié)里添加到Styles.css并且定義好的。
添加了這些標(biāo)簽之后,Visual Studio設(shè)計(jì)器里看起來(lái)應(yīng)該類似于圖18:
圖 18: 兩個(gè)Label控件添加到頁(yè)面
這些Label服務(wù)器控件放置到適當(dāng)?shù)奈恢煤?#xff0c;我們準(zhǔn)備好檢測(cè)當(dāng)并發(fā)沖突發(fā)生時(shí)如何判定,在哪個(gè)時(shí)間點(diǎn)把適當(dāng)?shù)?/span>Label的Visible屬性設(shè)置為true并顯示提示信息。
?
更新時(shí)處理并發(fā)沖突
讓我們首先看看當(dāng)使用批量更新模式是如何處理并發(fā)沖突。因?yàn)榕扛履J较碌倪@些沖突導(dǎo)致拋出一個(gè)DBConcurrencyException異常,我們需要在ASP.NET頁(yè)面中添加代碼來(lái)判定更新過(guò)程中出現(xiàn)的是否DBConcurrencyException異常。如果是,我們則顯示一個(gè)信息向用戶解釋他們的更改沒有被保存,由于別的用戶在他開始編輯和點(diǎn)擊更新按鈕之間的時(shí)間里修改了同樣的數(shù)據(jù)記錄。
正如我們?cè)?/span>在ASP.NET頁(yè)面中處理BLL/DAL異常 這一節(jié)里看過(guò)的那樣,這樣的異??梢栽跀?shù)據(jù)Web服務(wù)器控件的post級(jí)事件處理里被發(fā)現(xiàn)和排除。因此,我們需要?jiǎng)?chuàng)建一個(gè)GridView的RowUpdated事件的處理,它用來(lái)檢測(cè)是否拋出了一個(gè)DBConcurrencyException異常。這個(gè)事件處理通過(guò)一個(gè)不同的分支區(qū)別更新過(guò)程中引發(fā)的其它異常,如下面的時(shí)間處理代碼所示:
?
?1protected?void?ProductsGrid_RowUpdated(object?sender,?GridViewUpdatedEventArgs?e)?2{
?3????if?(e.Exception?!=?null?&&?e.Exception.InnerException?!=?null)
?4????{
?5????????if?(e.Exception.InnerException?is?System.Data.DBConcurrencyException)
?6????????{
?7????????????//?Display?the?warning?message?and?note?that?the?
?8????????????//?exception?has?been?handled
?9????????????UpdateConflictMessage.Visible?=?true;
10????????????e.ExceptionHandled?=?true;
11????????}
12????}
13}
?
面對(duì)一個(gè)DBConcurrencyException異常,該事件處理顯示UpdateConflictMessage Label控件并且指出該異常已經(jīng)被處理。正確地編寫了這些代碼后,當(dāng)更新記錄時(shí)發(fā)生了并發(fā)沖突,用戶的更改會(huì)丟失,因?yàn)樗麄儾荒芨采w同時(shí)發(fā)生的另一個(gè)用戶的更改。特別地,GridView回到預(yù)編輯幢白并且綁定到當(dāng)前數(shù)據(jù)庫(kù)中數(shù)據(jù)。這將在GridView的行中顯示出別的用戶的更改,而之前這些更改是看不見的。另外,UpdateConflictMessage Label控件將向用戶說(shuō)明發(fā)生了什么。圖19詳細(xì)展示了這一連串的事件。
圖 19: 面對(duì)并發(fā)沖突,一個(gè)用戶的更改丟失了
注意:作為另一種選擇,與其讓GridView回到預(yù)編輯狀態(tài),我們還不如讓GridView停留在編輯狀態(tài),通過(guò)設(shè)置傳入的GridViewUpdatedEventArgs對(duì)象的KeepInEditMode屬性為true。如果你接受這種方法,那么,必須重新綁定數(shù)據(jù)到GridView(通過(guò)調(diào)用它的DataBind()方法)從而將其他用戶更改后的值栽入到編輯界面。在這一節(jié)的可下載的代碼里,RowUpdated事件處理里有這兩行注悉掉的代碼;僅僅需要啟用這兩行代碼就可以讓GridView在發(fā)生了并發(fā)沖突之后保留編輯模式。
?
響應(yīng)刪除時(shí)的并發(fā)沖突
對(duì)于直接發(fā)送到數(shù)據(jù)庫(kù)的模式,面對(duì)并發(fā)沖突時(shí)并不會(huì)引發(fā)異常。然而,數(shù)據(jù)庫(kù)語(yǔ)句不影響任何記錄,因?yàn)?/span>WHERE子句不能匹配任何記錄。所有在BLL里創(chuàng)建的修改數(shù)據(jù)的方法都被設(shè)計(jì)為返回一個(gè)布爾值指示它們是否正好影響了一條記錄。因此,為了確定刪除記錄時(shí)是否發(fā)生了并發(fā)沖突,我們可以檢查BLL的DeleteProduct方法的返回值。
BLL方法的返回值可以在ObjectDataSource的post級(jí)事件處理中通過(guò)傳入事件處理的ObjectDataSourceStatusEventArgs對(duì)象的ReturnValue屬性被檢測(cè)。因?yàn)槲覀兏信d趣的是判斷從DeleteProduct方法返回的結(jié)果,我們需要?jiǎng)?chuàng)建一個(gè)ObjectDataSource的Deleted事件的事件處理程序。該ReturnValue屬性是object類型的,并且如果在方法可以返回一個(gè)值之前引發(fā)了異常并且方法被中斷的情況下,它的值也可能為null。所以,我們應(yīng)該首先確保ReturnValue屬性非空并是個(gè)布爾值。若能通過(guò)這個(gè)檢查,如果ReturnValue是 false我們顯示DeleteConflictMessage Label控件??梢酝ㄟ^(guò)下面的代碼完成:
?
?1protected?void?ProductsOptimisticConcurrencyDataSource_Deleted(object?sender,?ObjectDataSourceStatusEventArgs?e)?2{
?3????if?(e.ReturnValue?!=?null?&&?e.ReturnValue?is?bool)
?4????{
?5????????bool?deleteReturnValue?=?(bool)e.ReturnValue;
?6
?7????????if?(deleteReturnValue?==?false)
?8????????{
?9????????????//?No?row?was?deleted,?display?the?warning?message
10????????????DeleteConflictMessage.Visible?=?true;
11????????}
12????}
13}
14
?
面對(duì)一個(gè)并發(fā)沖突,用戶的刪除請(qǐng)求會(huì)被取消。GridView被刷新,顯示在用戶載入頁(yè)面跟點(diǎn)擊刪除按鈕之間的時(shí)間里發(fā)生在該記錄上面的更改。當(dāng)發(fā)生這樣的一個(gè)沖突,顯示DeleteConflictMessage Label控件,說(shuō)明發(fā)生了什么(見圖20)。
圖 20: 面對(duì)并發(fā)沖突,一個(gè)用戶的刪除請(qǐng)求被取消了
?
總結(jié)
并發(fā)沖突可能存在于所有允許多用戶同時(shí)更新或刪除數(shù)據(jù)的應(yīng)用程序里。如果不解決這樣的沖突,當(dāng)兩個(gè)用戶同時(shí)更新同一條數(shù)據(jù),無(wú)論誰(shuí)最后得到“勝利”,都將覆蓋掉另一個(gè)用戶所做的更改。作為另一種選擇,開發(fā)者可以實(shí)現(xiàn)開放式并發(fā)控制(optimistic concurrency control),或者保守式并發(fā)控制(pessimistic concurrency control)。開放式并發(fā)控制假定并發(fā)沖突很少發(fā)生,簡(jiǎn)單地否決一個(gè)會(huì)提起并發(fā)沖突的更新或者刪除命名。保守式并發(fā)控制則假定并發(fā)沖突頻繁地發(fā)生,簡(jiǎn)單地拒絕某個(gè)用戶的更新或者刪除命令是不可接受的。在保守式并發(fā)控制下,編輯一條記錄涉及到鎖定它,從而該記錄被鎖定時(shí)預(yù)防其他用戶的修改或刪除。
?
.NET中的類型化數(shù)據(jù)集提供了支持開放式并發(fā)控制的功能。特別地,發(fā)送到數(shù)據(jù)庫(kù)的UPDATE和DELETE語(yǔ)句包含了這個(gè)表的所有字段,從而確保了僅當(dāng)該記錄但前的值與用戶開始他們的修改或更新時(shí)的原始值相匹配時(shí),修改或刪除才會(huì)發(fā)生。一旦DAL配置為支持開放式并發(fā),BLL的方法就需要修改。另外,調(diào)用BLL的ASP.NET頁(yè)面也需要配置為ObjectDataSource能從它的數(shù)據(jù)Web服務(wù)器控件獲取到這些原始的值并將這些值傳送到BLL。
?
正如我們?cè)诒竟?jié)里所看到的,在ASP.NET web應(yīng)用程序中實(shí)現(xiàn)開放式并發(fā)控制包括修改DAL和BLL,還包括在ASP.NET頁(yè)面中添加相應(yīng)的支持。無(wú)論這些額外的工作對(duì)你的時(shí)間來(lái)說(shuō)是否一項(xiàng)明智的投入,對(duì)你的應(yīng)用程序來(lái)說(shuō)是否有所成效。如果你極少面對(duì)多個(gè)用戶同時(shí)更新數(shù)據(jù),或者不同的用戶對(duì)數(shù)據(jù)作出不同的更改,那么并發(fā)控制并非必選項(xiàng)。然而,如果你時(shí)常面對(duì)多個(gè)用戶在線并且對(duì)同一些數(shù)據(jù)進(jìn)行操作,并發(fā)控制可以幫助預(yù)防一個(gè)用戶的更新或刪除被另一個(gè)用戶在不知情的情況下覆蓋。
?
祝編程快樂(lè)!
?
作者簡(jiǎn)介
Scott Mitchell,著有六本ASP/ASP.NET方面的書,是4GuysFromRolla.com的創(chuàng)始人,自1998年以來(lái)一直應(yīng)用微軟Web技術(shù)。Scott是個(gè)獨(dú)立的技 術(shù)咨詢顧問(wèn),培訓(xùn)師,作家,最近完成了將由Sams出版社出版的新作,24小時(shí)內(nèi)精通ASP.NET 2.0。他的聯(lián)系電郵為mitchell@4guysfromrolla.com,也可以通過(guò)他的博客http://ScottOnWriting.NET與他聯(lián)系。
?
?
?
總結(jié)
以上是生活随笔為你收集整理的Scott Mitchell 的ASP.NET 2.0数据教程之二十一:: 实现开放式并发的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JEECG 3.2版本发布,基于代码生成
- 下一篇: Hadoop框架:MapReduce基本