ADO.NET 決不只是讓您能夠從數據庫中檢索數據或向數據庫中存儲數據。它還具有許多功能,可對數 據進行操作,以用於業務邏輯分析和在實際的應用程序中顯示。正如本專欄中的示例所示,ADO.NET 並不 僅僅是一個數據存取工具,還可用於數據操作。
在本月的專欄中,我將專門回答一些有關使用 ADO.NET 進行數據操作的常見問題。我將講述如何使用 基於表達式的列和 DataRelations 來達到特定目的。此外,我還將講述如何使用其他功能(例如 DataTable 的 Compute 方法和 SetOrdinal 方法)來滿足一般業務需要。
問:我想使用 DataTable 逐個訂單地顯示訂單信息並將其顯示在匯總分組(按天、月和年)中。我希 望能夠編輯訂單信息,而且父 DataTable 中的合計必須立刻反映出所做的更改。我希望每一組數據都顯 示在該窗體的單獨的網格中。在 DataTable 中加載了數據之後,如何使用 ADO.NET DataColumn 表達式 創建帶有每組訂單小計的分組信息?
問:我想使用 DataTable 逐個訂單地顯示訂單信息並將其顯示在匯總分組(按天、月和年)中。我希 望能夠編輯訂單信息,而且父 DataTable 中的合計必須立刻反映出所做的更改。我希望每一組數據都顯 示在該窗體的單獨的網格中。在 DataTable 中加載了數據之後,如何使用 ADO.NET DataColumn 表達式 創建帶有每組訂單小計的分組信息?
答:您可以輕松創建計算字段,方法是使用 SQL 語句中的計算表達式或創建一個綁定了表達式的 DataColumn。這兩種方法的原理和它們呈現的功能有所不同。
答:您可以輕松創建計算字段,方法是使用 SQL 語句中的計算表達式或創建一個綁定了表達式的 DataColumn。這兩種方法的原理和它們呈現的功能有所不同。
對於您的情況和要求,SQL 語句中的計算列不是最有效的方法。最好將基於表達式的 DataColumn 添 加到一系列 DataTable 中。
在深入講述此問題之前,我們先看一下我們要實現的最終結果(參見圖 1)。此示例在一個窗體中顯 示了五個網格。最頂部的網絡包含單獨的訂單行項。此網格中每個訂單行的價格、數量和折扣都可以編輯 。在編輯了這些值後,系統會為該行計算擴展價格,其他四個網格中的合計值也會自動重新計算並顯示最 新的合計以反映出所作的更改。在第二個網格中,每個訂單占一行,包括顯示該訂單的擴展價格小計的計 算列。第三、第四和第五個網格分別按天、月和年顯示每個訂單的小計。
圖 1 按天、月和年的訂單合計
每個網格都綁定到不同的 DataTable。第一個 DataTable 使用來自數據庫的訂單行進行加載,並追加 了一個基於表達式的列。在使用 DataAdapter 的 Fill 方法將數據加載到 DataTable 中後,會向 DataTable 添加一個名為 ExtendedPrice 的 decimal 類型的列。該列定義為表達式列,它根據每個 DataRow 的“單價”、“數量”和“折扣”值計算該列的值。
ds.Tables["OrderLines"].Columns.Add("ExtendedPrice", typeof(decimal), "(UnitPrice * Quantity) * (1 - Discount)");
OrderLines DataTable 包含此窗體的核心數據。一旦加載了它並創建了它的表達式列,就可以填充第 一組 DataTable 並將其添加到 DataSet 中(參見圖 2)。SQL 語句會提取每個訂單的 OrderID 和 OrderDate,然後用結果填充 DataTable。在將 OrderTotal 列添加到此 DataTable 之前,必須先創建 DataRelation,以便此 Orders DataTable 中的表達式能夠訪問 OrderLines DataTable 中的列。
Figure 2 求和的表達式列
using(SqlCommand cmd = new SqlCommand("SELECT o.OrderID, o.OrderDate " + "FROM Orders o WHERE YEAR(o.OrderDate) = @year " + "ORDER BY o.OrderDate DESC", cn)) { cmd.CommandType = CommandType.Text; cmd.Parameters.AddWithValue("@year", year); SqlDataAdapter adpt = new SqlDataAdapter(cmd); DataTable orders = new DataTable("Orders"); adpt.Fill(orders); } ds.Tables.Add(orders); ds.Relations.Add("Orders2OrderLines", ds.Tables["Orders"].Columns["OrderID"], ds.Tables ["OrderLines"].Columns["OrderID"]); orders.Columns.Add("OrderTotal", typeof(decimal), "Sum(Child.ExtendedPrice)");
在兩個 DataTable 的 OrderID 列之間建立 DataRelation。然後,可以創建 OrderTotal 表達式列並 將其添加到 Orders DataTable:
Sum(Child.ExtendedPrice)
運算先從括號內進行,然後是括號外,Child 運算符會指示 OrderTotal DataColumn 向下找到子表 (OrderLines DataTable) 並獲取 ExtendedPrice 列。然後,Sum 運算符再次使用 DataRelation 將 OrderLines DataTable 中 OrderID 值與父 DataTable 的 OrderID 相符的各行的 ExtendedPrice 列的 值相加。最終的結果就是 Orders DataTable 的 OrderTotal 列中顯示出每個訂單的總金額。
請記住,如果有多個 DataRelation,則需要更改語法,以指明具體的 DataRelation。圖 2 中的示例 可更改為:
Sum(Child(Orders2OrderLines).ExtendedPrice)
您可以使用同樣的方法來填充 OrdersByDay DataTable,並使用來自數據庫中的不同的訂單日期列表 來加載它。然後在此 DataTable 和它的 OrderDate 列與 Orders DataTable 的 OrderDate 列之間建立 DataRelation。這樣,OrdersByDay DataTable 就可以將訂單合計匯總到 OrderTotalByDay 列,如以下 所示:
ds.Relations.Add("OrdersByDay2Orders", ds.Tables["OrdersByDay"].Columns["OrderDate"], ds.Tables["Orders"].Columns["OrderDate"]); dayOrders.Columns.Add("OrderTotalByDay", typeof(decimal), "Sum(Child.OrderTotal)");
最後兩個窗格被綁定到以相同方式加載的 DataTable。您可以拓展此方法以顯示其他計算值。例如, 您可以輕松地顯示包含特定客戶的訂單合計的網格,只要該客戶的信息可以在基表中檢索到,就能顯示。 使用 DataView,還可以針對某個表達式列篩選網格。
問:我有一個父 DataTable (Orders) 和一個子 DataTable (Order Lines)。父 DataTable 有一個針 對 OrderTotal 的表達式列。我需要將父表中每一行的某幾列顯示在子表中。如何才能從父表中提取值?
問:我有一個父 DataTable (Orders) 和一個子 DataTable (Order Lines)。父 DataTable 有一個針 對 OrderTotal 的表達式列。我需要將父表中每一行的某幾列顯示在子表中。如何才能從父表中提取值?
基於表達式的列可以使用 Child 運算符訪問子 DataTable 中的列。不過,它們也可以使用 Parent 運算符來訪問父 DataTable 中的列。例如,若要訪問父表中某列的值,您可以對先前的示例中的代碼進 行修改,增加以下代碼片段:
ds.Tables["OrderLines"].Columns.Add("OrderTotalForOrderLinesTable", typeof(decimal), "Parent.OrderTotal");
基於表達式的列可以使用 Child 運算符訪問子 DataTable 中的列。不過,它們也可以使用 Parent 運算符來訪問父 DataTable 中的列。例如,若要訪問父表中某列的值,您可以對先前的示例中的代碼進 行修改,增加以下代碼片段:
ds.Tables["OrderLines"].Columns.Add("OrderTotalForOrderLinesTable", typeof(decimal), "Parent.OrderTotal");
此代碼將一個表達式列添加到名為 OrderLines 的子表,該子表從 Orders DataTable 中提取 OrderTotal 值。在創建此新列之前,必須先創建子 DataTable 和父 DataTable,建立這兩表之間的 DataRelation 並創建 OrderTotal 列。這是基於子表創建將綁定到某網格的 DataView 的常用方法。
當您將 Parent 運算符用於表達式列時,等於將父 DataTable 中的數據復制到子 DataTable。由於這 些表是同步的,所以,如果有人對父 DataTable 中的值進行了更改,則子表中的相應值也會顯示這一更 改。
問:我有一個 DataGridView 綁定到了 DataTable 的 DefaultView。該 DataTable 的數據源是一個 存儲過程,該存儲過程從我的應用程序的數據庫中檢索數據。所有這些字段都以特定的順序返回,但我需 要在我的 DataGridView 中以另一種順序顯示這些字段。如何才能輕松地在 DataGridView 中對這些列進 行重新排序?
問:我有一個 DataGridView 綁定到了 DataTable 的 DefaultView。該 DataTable 的數據源是一個 存儲過程,該存儲過程從我的應用程序的數據庫中檢索數據。所有這些字段都以特定的順序返回,但我需 要在我的 DataGridView 中以另一種順序顯示這些字段。如何才能輕松地在 DataGridView 中對這些列進 行重新排序?
答:有時候您會發現,當更改 SQL 語句中的字段的順序不太現實時,便有必要更改 DataTable 中的 列順序。DataColumn 公開了一個 SetOrdinal 方法,可用於更改 DataTable 中的各列的順序。
答:有時候您會發現,當更改 SQL 語句中的字段的順序不太現實時,便有必要更改 DataTable 中的 列順序。DataColumn 公開了一個 SetOrdinal 方法,可用於更改 DataTable 中的各列的順序。
序號位置從 0 開始計算,因此,要將第一列挪為第四列,需要使用以下代碼:
myDataTable.Columns[0].SetOrdinal(3);
如果您願意,可以通過列的名稱而不是其序號位置來引用列。
myDataTable.Columns["OrderID"].SetOrdinal(3);
第三個序號位置之前的各列都遞減一個位置,以便為位置更改騰出空間。
問:我的屏幕上顯示了一個客戶列表。用戶可以從此列表中選擇特定的客戶,從而顯示該客戶的所有 數據。然後,用戶可以從此列表中選擇另一個客戶,查看該客戶的信息。我可以一次將所有客戶數據加載 到某個 DataTable 中,從而只訪問該數據庫一次;也可以只提取包含客戶及其 ID 的列表,然後在用戶 選擇某客戶時從數據庫中獲取該客戶的相關信息。請問這兩個方法哪個更好?
問:我的屏幕上顯示了一個客戶列表。用戶可以從此列表中選擇特定的客戶,從而顯示該客戶的所有 數據。然後,用戶可以從此列表中選擇另一個客戶,查看該客戶的信息。我可以一次將所有客戶數據加載 到某個 DataTable 中,從而只訪問該數據庫一次;也可以只提取包含客戶及其 ID 的列表,然後在用戶 選擇某客戶時從數據庫中獲取該客戶的相關信息。請問這兩個方法哪個更好?
答:這個問題很普遍。是應該進行一次較大的訪問,還是進行多次較小的訪問呢?如果從數據庫中提 取一些列客戶及其全部信息,並將結果以 DataSet 形式保存在內存中,則需要檢索大量可能從不會用到 的信息。不過,也潛在地減少了對該數據庫的訪問次數。例如,如果客戶表中有 1000 位客戶,您提取了 每個客戶的 40 列數據,則會有 40000 個值檢索和存儲到 DataTable 中。相比之下,客戶列表只需要顯 示值(例如 CompanyName)和 ID(例如 CustomerID)即可。其余各列僅當用戶從客戶列表中選擇特定客 戶時才顯示。
答:這個問題很普遍。是應該進行一次較大的訪問,還是進行多次較小的訪問呢?如果從數據庫中提 取一些列客戶及其全部信息,並將結果以 DataSet 形式保存在內存中,則需要檢索大量可能從不會用到 的信息。不過,也潛在地減少了對該數據庫的訪問次數。例如,如果客戶表中有 1000 位客戶,您提取了 每個客戶的 40 列數據,則會有 40000 個值檢索和存儲到 DataTable 中。相比之下,客戶列表只需要顯 示值(例如 CompanyName)和 ID(例如 CustomerID)即可。其余各列僅當用戶從客戶列表中選擇特定客 戶時才顯示。
您描述的另一種方法是只加載包含 CompanyName 和 CustomerID 的客戶列表。然後,當用戶選擇某客 戶時,應用程序再次調用數據庫,從中檢索該特定客戶的詳細信息。此方法預先加載的數據明顯要少,但 訪問數據庫的次數更多。
哪個方法更好取決於其他因素。例如,如果客戶數據很少,很快就能收集到且不會使性能低於負載測 試,則預先收集所有數據可能更好。不過,如果數據更改非常頻繁,則此方法的主要缺陷是加載的數據很 快就會失效。因此,在選擇預先將所有數據加載到內存之前,首先應該考慮加載數據所需的時間、此數據 需要耗費多少內存、實際加載多少數據、加載時數據庫中的數據是否可能會更改(這會導致您屏幕上顯示 的數據過時)。通常,當數據集較小且後台不會更新時,我推薦采用此方法。
第二個方法是僅當用戶請求時才提取每個用戶的詳細信息。使用這種方法,列表的加載速度通常更快 ,因為加載的數據較少。而且,使用這種方法得到的數據是最新的,因為唯一不變的數據是客戶列表。這 種方法的不足之處是:每次用戶選擇一個客戶時,應用程序都要訪問數據庫。因此,應該對獲取客戶詳細 信息的調用進行優化,因為您是使用鍵字段 (CustomerID) 來獲取客戶數據。如果這些檢索客戶數據的調 用導致了明顯的延遲,速度下降的原因可能是網絡帶寬、查詢本身或通過應用程序的體系結構傳送數據出 現了問題。如果您決定使用此方法,但之後卻遇到了速度下降的問題,則必須確定速度變慢的原因以及該 問題是否能解決。
最後要說的是,如何做出此類決定絕不是三言兩語就能回答的。大部分情況下,我們只能視具體情況 而定。不過,一般情況下,我發現較好的方法是,先加載包含最少數據的列表,然後在用戶選擇特定的項 時再回去檢索其他數據。 要知道,性能不是要考慮的唯一問題,您還需要考慮數據是否過時的問題。
問:我需要呈現已綁定到 DataTable 並加載了銷售信息的 DataGridView。我的應用程序必須允許用 戶對該網格中的銷售數據進行計算,並允許他們選擇將哪些行和列包含在計算中。如何才能做到在不用再 次訪問數據庫的情況下對 DataTable 進行計算?
問:我需要呈現已綁定到 DataTable 並加載了銷售信息的 DataGridView。我的應用程序必須允許用 戶對該網格中的銷售數據進行計算,並允許他們選擇將哪些行和列包含在計算中。如何才能做到在不用再 次訪問數據庫的情況下對 DataTable 進行計算?
答:這種情況下,Compute 方法可以很好地解決問題。基於表達式的列一次只能對一行執行計算,而 使用 DataTable 的 Compute 方法卻可以在給定篩選器和表達式的條件下對一組行執行計算。篩選器用於 限定 Compute 方法應對哪些行執行計算,而表達式則給出了要執行的聚合函數表達式。例如,您可以向 DataTable 添加一列,用於計算美國客戶的平均訂單合計。這個工具非常好,不需要再次訪問數據庫即可 快速對一組行執行計算。
答:這種情況下,Compute 方法可以很好地解決問題。基於表達式的列一次只能對一行執行計算,而 使用 DataTable 的 Compute 方法卻可以在給定篩選器和表達式的條件下對一組行執行計算。篩選器用於 限定 Compute 方法應對哪些行執行計算,而表達式則給出了要執行的聚合函數表達式。例如,您可以向 DataTable 添加一列,用於計算美國客戶的平均訂單合計。這個工具非常好,不需要再次訪問數據庫即可 快速對一組行執行計算。
圖 3 顯示的示例窗體包含一個訂單列表以及每個訂單的合計。此示例包含了表達式和篩選器所需的所 有基本要素。在左側的“篩選器”組合框中,用戶可以選擇要篩選的字段、要應用於該字段的運算符以及 篩選條件。通過綁定到 DataColumn 的列表填充字段列表:
DataColumn[] colList = new DataColumn[orders.Columns.Count]; orders.Columns.CopyTo(colList, 0); ddlFilterColumn.DataSource = colList; ddlFilterColumn.DisplayMember = "ColumnName";
圖 3使用 Compute 方法計算
此代碼創建了一個 DataColumn 數組,使用 DataTable 中的列填充它並將其綁定到組合框。通過將 DataColumn 數組綁定到組合框,可以訪問能夠使用的所選 DataColumn 的其他屬性。例如,如果所選列 是一個字符串,則該值必須加上引號。由於 DataColumn 已綁定到該組合框,所以此代碼能判斷出是否應 為該值加引號。
Calculation 組合框包含一個聚合函數列表和一個所選聚合函數要運算的列的列表。列列表綁定到 ddlCalculationColumn 組合框的方式與綁定到 ddlFilterColumn 組合框的方式相同。聚合函數列表加載 有以下七個可供 Compute 方法使用的函數:Sum、Count、Avg、Min、Max、StDev 和 Var。
單擊該按鈕,便會執行圖 4 中顯示的代碼。它首先確定為篩選器所選的列的類型。如果列的類型為字 符串或日期,則會為條件值加引號。形成了篩選器和表達式之後,系統會將它們傳遞給 Compute 方法, 結果將發送到文本框。
Figure 4 執行計算
if (!ValidateControls()) return; Type colType = ((DataColumn) (ddlFilterColumn.SelectedItem)).DataType; string filterCriteria = string.Empty; if (ddlFilterOperator.Text == "Like") { filterCriteria = string.Format("'{0}%'", txtFilterCriteria.Text); } else { filterCriteria = (colType.Name == "String" || colType.Name == "DateTime") ? string.Format("'{0}'", txtFilterCriteria.Text) : txtFilterCriteria.Text; } string filter = string.Format("{0} {1} {2}", ddlFilterColumn.Text, ddlFilterOperator.Text, filterCriteria); string expression = string.Format("{0}({1})", ddlAggregate.Text, ddlCalculationColumn.Text); object value = this.orders.Compute(expression, filter); txtResult.Text = value.ToString();
將您想向 John 詢問的問題和提出的意見發送至 [email protected].
本文配套源碼:http://www.bianceng.net/dotnet/201212/766.htm