DataReader--優點

---他山之石,可以攻玉。

http://editblog.csdn.net/msdncolumn/archive/2005/02/25/1657.aspx

使用 DataReader 來提高速度並減少內存使用

作者:Rick Dobson
相關技術:ADO.NET、C#、數據庫開發
難度:★★★☆☆
讀者類型:.NET開發人員、數據庫開發人員

    [導讀]談及數據庫連接時,.NET的擁護者會力推數據適配器和數據集所提供的離線訪問的優勢。每每在這個時候,DataReader就會被這二者的光芒所掩蓋。但是,正如Rick Dobson在此處演示的那樣,DataReader絕非常物—它們提供對數據源的只進、只讀連線訪問,而且它們不支持數據操作。那麼,爲什麼還要使用如此束縛人的東西呢?答案是“性能”,對於入門者來說,使用DataReader要快很多;另一個好處是佔用的內存較少—DataReader可讓您在獲得數據的同時就對它進行處理,每次一行。所以,DataReader特別適用於處理過於龐大以致於無法加載到內存中的數據。

    爲了從DataReader獲得最大的利益,您需要了解它的功能和限制。由於DataReader具有定義完善的限定,您還可以瞭解一下如何利用其他.NET實體(例如“數組”)來補充DataReader的功能,並從中獲益。本文通過在三個範疇中各自的一些示例應用程序來回顧DataReader的功能。首先,我將展示生成、填充並配置DataReader以便用於Windows窗體控件的有效代碼模式;第二,對示例突出說明了如何使用類型化數據來計算表達式,這將反映DataReader中列的數據類型的特性;最後,我將比較通過DataReader來檢索分層數據的兩種技術,來爲本文劃上句號。

從DataReader到列表框

    您可以用指向數據源的DataReader來輕鬆地填充列表框。本部分的示例通常也應用於組合框控件。您應首先爲Command對象創建DataReader並調用ExecuteReader方法,該方法通常是內建的。ExecuteReader方法可以接受CommandBehavior枚舉來自定義Command的行爲以及它所關聯的DataReader。本部分中的兩個示例突出了DataReader及其Command對象之間的相互作用,並提供了有關窗體和控件管理的其他有趣的應用程序的詳細信息。請參閱HCVSDataReaders項目(源代碼中的HVSO4-07Dobon.exe),以訪問每個示例的所有代碼。

顯示原始的DataReader數據

    HCVSDataReaders項目中的第一個DataReader示例是在Form1上Button1的Click事件中,ADONETObjects類中的兩個方法和Form1後面的DataReaderForTable函數過程也在該事件中。爲方便起見,ADONETObjects類駐留在HCVSDataReaders項目中。圖1顯示單擊Populate from DataReader按鈕後的窗體。按鈕的Click事件過程用從SQL Server Northwind數據庫中的Employees表選定的列值來填充列表框。


圖1

    SqlDataReader類有很多特殊方法,用於從各種專用的.NET和SQL Server數據格式中獲取數據。不過,對於簡單的應用程序來說,您無須考慮它們。所有DataReader都需要做的事情是,接受從任何非字符串數據類型到字符串的默認轉換,然後將一個經過計算的字符串表達式添加到列表框。這就是下面的代碼所要做的事情,它來自Button1_Click過程。一個While循環逐行讀取,每次創建一個包含四個對drd1 DataReader的引用的str2表達式。這些引用中有兩個是數字實例。值甚至可以爲空(如2號僱員的ReportsTo列值)。不過,對每一行來說,該表達式都是成功的。您可以按名稱或基於零的索引來指定列。

Do While drd1.Read
    Dim str2 As String = _
        "Employee " & drd1("EmployeeID") & _
        ", " & drd1("FirstName") & _
        " " & drd1("LastName") & _
        " reports to: " & drd1("ReportsTo")
    ListBox1.Items.Add(str2)
Loop

    第一個示例中最有趣的部分可能是如何先創建drd1 DataReader。Button1的Click事件過程將drd1創建爲一個SqlDataReader類,並將我創建的名爲ataReaderForTable的函數的返回值指定給它。它傳遞Employees表的名稱—DataReaderForTable爲其開發了一個DataReader。

Dim drd1 As SqlClient.SqlDataReader = _
    DataReaderForTable("Employees")

    DataReaderForTable過程創建DataReader的步驟有三個。

Dim drd1 As SqlClient.SqlDataReader
Dim ADOObjs As New ADONETObjects

'Specify connection object
Dim cnn1 As SqlClient.SqlConnection = _
    ADOObjs.MakeNorthwindConnection

'Specify a command object
Dim str1 As String = _
    "SELECT * FROM " & TableName
Dim cmd1 As _
    SqlClient.SqlCommand = _
    ADOObjs.MakeACommand(cnn1, str1)

'Open cnn1 and create the drd1 DataReader
'with the ExecuteReader method cnn1.Open()
drd1 = cmd1.ExecuteReader _
    (CommandBehavior.CloseConnection)

Return drd1

    首先,它用我的ADONETObjects類的MakeNorthwindConnection方法創建一個到Northwind數據庫的連接。其次,我爲DataReader創建一個Command對象。DataReaderForTable過程將兩個參數傳遞到我的ADONETObjects的MakeACommand方法,以返回一個新的Command對象。這些參數用於爲傳遞到DataReaderForTable過程的TableName參數中的所有行提取所有列的SQL語句,以及MakeNorthwindConnection方法返回的Connection對象。在第三步中,該過程用ExcecuteReader方法爲Command對象實際創建DataReader。使用CommandBehavior.CloseConnection枚舉,可以使Button1_Click過程關閉返回到它的DataReader,而無須操作關聯的Connection對象。這是因爲枚舉指示.NET Framework在DataReader關閉時自動關閉Connection對象。DataReaderForTable過程通過返回實例化的DataReader來結束。順便提一下,DataReaderForTable過程有一個共享訪問模式聲明,以便整個HCVSDataReaders項目的其他模塊中的過程可以調用它。

處理DataReader數

    至少可以在兩個方面改進ListBox1的內容。第一,沒有EmployeeID值來指示Andrew Fuller向誰報告。這不是一個錯誤,因爲它不向列表框中的其他成員報告。但是,空白仍可能會使人產生困惑。第二,ListBox1按照經理的EmployeeID來指定僱員的經理。通過用經理的姓來替代其EmployeeID,可以提高ListBox1內容的可讀性。Button2_Click過程以使用Button1_Click過程處理這兩種問題的方法來填充ListBox1。圖2顯示在單擊Populate from array按鈕後改進的輸出。名爲Andrew Fuller的僱員的行表明他在列表中沒有主管。ListBox1中所有其他僱員的項顯示主管的姓而非EmployeeID。


圖2

    將主管的EmployeeID列值轉換爲姓時遇到的主要難題之一是,DataReader一次只分析某個僱員的一行。爲了轉換主管的EmployeeID列值,應用程序需要將每個EmployeeID值都鏈接到姓。通過將來自DataReader的值存儲到字符串的數組中,過程可以查找與EmployeeID值相匹配的姓。(當然,這一特定問題也可以通過在查詢中創建一個更復雜的Select語句來解決,但是,就演示將數組與DataReader配合使用而言,我將爲您展示如何在客戶端解決這個問題。)以下Button2_Click過程的代碼片段顯示如何用來自drd1 DataReader的值填充字符串值的MyEmps數組,這是以Button1_Click中的同一方法定義的。

Const RowsCount As Integer = 99
Dim MyEmps(RowsCount, 3) As String

Do While drd1.Read
    If int1 <= RowsCount Then
        For int2 = 0 To drd1.FieldCount() - 1
            Select Case drd1.GetName(int2)
                Case "EmployeeID"
                    MyEmps(int1, 0) = drd1(int2)
                Case "FirstName"
                    MyEmps(int1, 1) = drd1(int2)
                Case "LastName"
                    MyEmps(int1, 2) = drd1(int2)
                Case "ReportsTo"
                    'ToString method forces conversion --
                    'even for DBNull value to string
                    MyEmps(int1, 3) = drd1(int2).ToString
            End Select
        Next
        int1 += 1
    Else
        MessageBox.Show( _
            "Reset RowsCount to a larger number and re-run.", _
            "Terminal Error Message", _
            MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        Exit Sub
    End If
Loop

    MyEmps數組有4列,用於儲存EmployeeID、FirstName、LastName和ReportsTo列值。對於Northwind數據庫的默認行數(9行)來說,其最大的行數規範綽綽有餘。閱讀行的While循環中具有執行各種任務的代碼。For循環可循環訪問所有列值,以便爲MyEmps中的存儲選擇DataReader列值的一個子集。FieldCount屬性返回DataReader的列數。Select...End Select語句用GetName方法檢查DataReader的列名稱,以標識要將列值存儲到哪個MyEmps列。除了ReportsTo列值以外,這段代碼將應用默認的Visual Basic .NET轉換技術,將SQL Server數據格式轉換爲MyEmps數組中的.NET字符串格式。由於ReportsTo列可以包含空值(DBNULL),因此這個過程必須顯式指定的ToString方法,以將DBNULL強制爲字符串值—即空字符串("")。在收集MyEmps數組中的所有drd1列值以後,Button2_Click將關閉DataReader並釋放這些資源。下面顯示的主代碼片段將依次通過MyEmps的行來計算字符串表達式,以便顯示在ListBox1中。該代碼再一次順序通過MyEmps數組來查找匹配ReportsTo列值的LastName列值,而非只顯示行的原始ReportsTo列值。在進入循環以將ReportsTo列值解碼爲LastName列值之前,該代碼會確定第4列中的ReportsTo值是否爲空字符串。

For int1 = 0 To 99
    If MyEmps(int1, 0) <> "" Then
        If MyEmps(int1, 3) <> "" Then
            strSupvrEmpID = MyEmps(int1, 3)
            For int2 = MyEmps.GetLowerBound(0) To _
                MyEmps.GetUpperBound(0)

                If MyEmps(int2, 0) = strSupvrEmpID Then
                    strEmpID = MyEmps(int2, 2)
                    Exit For
                End If
            Next
        Else
            strEmpID = " no one in list box"
        End If
        str1 = "EmployeeID" & MyEmps(int1, 0) & _
            ", " & MyEmps(int1, 1) & " " & _
            MyEmps(int1, 2) & " reports to: " & _
            strEmpID
        ListBox1.Items.Add(str1)
    Else
        Exit For
    End If
Next

處理類型化數據

    Form1的應用程序將每個DataReader列的內容轉換爲一個字符串值,而不考慮數據源中列的基礎數據類型是什麼。有時您需要使用原始數據類型,比如當您需要對列值執行數值或數據算法時。如果您還不瞭解基礎數據類型,那麼在表達式中使用它們之前,您需要使用一種技術來找出原始數據類型。

報告列名稱和數據類型

    Form2中的Button1_Click過程演示了一種寫出任何DataReader的列名稱和數據類型的技術。儘管.NET爲此任務提供了其他方法,但該技術構建於您對DataReader的瞭解以及如何將它們與數組一起使用的基礎之上。該過程首先根據Form1中的DataReaderForTable過程,爲Northwind數據庫中的Orders表創建DataReader。由於DataReaderForTable過程是用共享訪問模式聲明的,因此Form2可用以下代碼調用:

Dim drd1 As SqlClient.SqlDataReader = _
    Form1.DataReaderForTable("Orders")

    您還需要一個數組來保存drd1 DataReader的列名稱和數據類型。數組列將保存爲drd1 DataReader指定列的名稱及其數據類型的字符串值。下面的代碼顯示瞭如何應用Array類的CreateInstance共享方法,以創建一個名爲OrdersColNamesTypes的數組。在drd1 DataReader中有多少列,這個數組就有多少行,另外還有兩個列。For循環可循環訪問DataReader的列,以便用列名稱和數據類型元數據填充該數組。SetValue方法爲數組元素指定值。您可以從上述示例瞭解如何使用GetName方法返回列名稱。這個過程闡釋瞭如何應用GetDataTypeName方法來恢復DataReader中列的原生數據類型名稱。

Dim OrdersColNamesTypes As Array = _
    Array.CreateInstance(GetType(String), _
    drd1.FieldCount, 2)

For int1 As Integer = 0 To drd1.FieldCount - 1
    OrdersColNamesTypes.SetValue _
        (drd1.GetName(int1), int1, 0)
    OrdersColNamesTypes.SetValue _
        (drd1.GetDataTypeName(int1), int1, 1)
Next


圖3

    如您在圖3中看到的那樣,Form2上的Button1_Click過程的最後代碼片段只是依次通過OrdersColNamesTypes數組中每個連續行的列值,並將列名稱和數據類型打印到“Output”窗口。圖中的報表表明Order表有14列,Order表的第一列名爲OrderID,數據類型爲SQL Server int。其他列包含變化和固定長度的字符串數據類型(nvarchar和nchar)以及datetime和money數據類型。

執行算法

    對DataReader列值執行算法的竅門是,將它們保存爲與其原生數據庫數據類型相匹配的Visual Basic .NET數據類型。不過,數組會將所有元素成員強制轉換爲同一類型。使用數組存儲DataReader的值,但仍保持數據源數據類型的一種方法是,將DataReader列值保存到一個具有Object數據類型元素的數組中。從本質上說,這個過程將DataReader列值“裝箱”爲(而非將其強制轉換爲)另一種數據類型的Object實例。稍後,您可以通過將數組元素指定給用適當數據類型聲明的變量,來恢復基本的基礎數據格式。從本質上說,這個指定取消裝箱已封裝了的數據類型。Form2之後的代碼包括一個過程—PopArray,它將DataReader列值裝箱到一個帶有Object元素的數組中。如果您對這個過程的詳細信息感興趣,請查看HCVSDataReaders項目中的PopArray列表。在本文中,PopArray過程的一個主要目的是,用Windows應用程序中的Orders表的列值來演示integer和datetime算法。Form2的Button2_Click過程有兩個主代碼片段。第一個演示瞭如何計算Orders數組中第一行和最後一行OrderID列值之間的差,這將鏡像化Northwind數據庫中的Orders表。在開始執行第一個主代碼片段之前,該過程會調用PopArray過程來填充Orders數組,正如您所知道的,Orders表有830行。對名爲int1和int2的兩個變量的指定爲Orders數組第一列中的第一行和最後一行取消裝箱了的OrderID列值。WriteLine方法的參數包括一個從其他Integer變量中減去一個Integer變量的簡單表達式。

Dim Orders As Array = PopArray("Orders", 830)

Dim int1 As Integer = _
    Orders(Orders.GetLowerBound(0), _
Orders.GetLowerBound(1))
Dim int2 As Integer = _
    Orders(Orders.GetUpperBound(0), _
    Orders.GetLowerBound(1))
Console.WriteLine(ControlChars.CrLf & _
    "An example with integer arithmetic:")
Console.WriteLine( _
    "There are {2} order numbers between " & _
    "the first order number({0}) and the " & _
    "last order number({1})", _
    int1, int2, int2 - int1)

    Button2_Click的第二個主代碼片段對Orders數組第一行中的ShippedDate和RequiredDate列值執行datetime算法。這段代碼將兩列取消裝箱爲Date數據類型,而不是將Object元素取消裝箱爲Integer數據類型的變量。您可以交替使用Date和Datetime關鍵字,在Visual Basic .NET中指定datetime值。DateDiff函數計算兩個datetime變量之間的天數差。Console類的WriteLine方法將結果顯示在“Output”窗口中。

'Demonstrate arithmetic with dates
Dim datRequired As Date = Orders(0, 4)
Dim datShipped As Date = Orders(0, 5)
Console.WriteLine(ControlChars.CrLf & _
    "An example with date arithmetic")
Console.WriteLine( _
    "Required date({1}) - ShippedDate({0}) " & _
    "= {2} days", _
    datShipped.ToString("M/d/yyyy"), _
    datRequired.ToString("M/d/yyyy"), _
    DateDiff(DateInterval.Day, datShipped, _
    datRequired))

生成分層數據

    對應用程序而言,對分層數據(如屬於某個訂單的行項)的需求使用是很常見的。最後的兩個示例展示了兩種通過DataReader返回分層數據的方法。一種方法演示瞭如何使用專用的MSDataShape提供程序。第二種方法在本文前面所演示的工具的基礎上,使用了更多常規工具。另外,通過在相關表中添加值的查找功能以及闡釋datetime和currency值的格式化語法,可以使第二種技術建立在第一種之上。

使用MSDataShape provider

    正如我之前指出的,MSDataShape provider是一種用於返回分層數據的專用provider。這個provider要回溯到Visual Basic 6,但Microsoft發表了一篇知識庫文章,描述如何在Visual Basic .NET和ADO.NET中使用MSDataShape provider(http://support.microsoft.com/default.aspx?scid=kb;[LN];308045)。雖然MSDataShape provider在返回分層結果集方面格外有效,但它依賴於SQL的子集以及專用關鍵字和其他語法約定。另外,這個provider不能與.NET SQL Server data provider一起使用。甚至在您處理SQL Server數據庫的時候,將被迫改爲使用OleDb .NET data provider。使用MSDataShape provider建立到數據庫的連接與使用其他的略有不同。下面的代碼來自Form3中的Button1_Click過程。請注意,該代碼在OleDb命名空間中指定了一個Connection對象。儘管服務器、集成安全性和初始目錄的最後3個參數與SqlConnection對象連接字符串的那些參數一樣,但最初的兩個參數截然不同。最前面的參數指定了MSDataShape provider,該provider與第二個參數中指定的SQLOLEDB data provider協同工作。

New OleDb.OleDbConnection( _
    "Provider=MSDataShape;Data Provider=SQLOLEDB;" & _
    "server=(local);Integrated Security=SSPI;" & _
    "Initial Catalog=northwind")

    接下來的3段代碼塊闡釋了指定Command對象的語法,該對象基於Northwind數據庫的Orders表和Order Details表生成分層結果集。

Dim cmd1 As OleDb.OleDbCommand = _
    New OleDb.OleDbCommand( _
    "SHAPE {SELECT OrderID, OrderDate " & _
    "FROM Orders " & _
    "WHERE OrderID=" & TextBox1.Text & "} " & _
    " APPEND ({SELECT OrderID, ProductID, " & _
    "UnitPrice, Quantity, Discount " & _
    "FROM [Order Details]} " & _
    " RELATE OrderID TO OrderID)", cnn1)

cnn1.Open()
Dim drd1 As OleDb.OleDbDataReader = _
    cmd1.ExecuteReader(CommandBehavior.CloseConnection)
drd1.Read()
Console.WriteLine("{0}, {1}", _
    drd1(0), drd1(1))

Dim drd2 As OleDb.OleDbDataReader = drd1(2)
Do While drd2.Read
Console.WriteLine("{0}, {1}, {2}, {3}, {4}", _
    drd2(0), drd2(1), drd2(2), drd2(3), drd2(4))
Loop

    請注意專用關鍵字SHAPE、APPEND和RELATE。SHAPE子句的SQL語句指定主結果集的行。這個語句引用TextBox1的Text屬性,該屬性應該始終指定有效的OrderID列值。APPEND子句的SQL語句指定分層結果集的明細成員的結果集。RELATE子句指示在哪些列上匹配主數據源和明細數據源中的行。在實例化Command對象後,代碼將通過打開cnn1 Connection對象來準備生成幾個DataReader。drd1 DataReader從主數據源返回數據,drd2 DataReader從明細數據源提取數據。主數據源的Console.WriteLine語句打印主數據源的前兩個列值,它們是OrderID和OrderDate。明細數據源的Console.WriteLine語句打印Order Details表的所有行,其OrderID匹配TextBox1中顯示的值。


圖4

    圖4 顯示在單擊Shape按鈕後的Form3。窗體下的“Output”窗口表示分層結果集。第一行顯示主數據源的行,包括OrderID和OrderDate列值;接下來的3行顯示OrderID值爲10248的訂單的明細行項目;第二列和第三列是用於ProductID和UnitPrice列值的。打印ProductID列值(而非ProductName列值)使得辨別每個行項目引用了哪個產品變得更困難。此外,從輸出不能明顯看出UnitPrice列值是貨幣值。

用常規工具返回分層結果集

    返回分層數據的第二個示例依賴於常規工具,如那些已經在本文中展示過的工具的改編本。第二個示例的詳細代碼顯示在Button2_Click過程中,以及HCVSDataReaders項目的Form3模塊中名爲ComputerArrayIndex的相關過程中。返回分層數據的第二種方法基於Northwind數據庫中的Orders、Order Details和Products表的關聯DataReader,創建了三個數組。以這種方法使用數組可以減少數據庫服務器上的負載,這是因爲它允許應用程序關閉DataReader及其到數據源的關聯Connection。下面的代碼片段來自Form3模塊中的Button2_Click,它闡釋了用來生成Orders數組的方法。Form2中的PopArray過程已經在前面簡要描述過了。它基於Northwind數據庫的DataReader生成數組,您將要閱讀的最大行數以及表名稱也會被傳遞給它。順便說一下,PopArray過程在填充數組後會關閉它的DataReader。ComputeArrayIndex過程從一個二維數組(如Orders)的第一列生成一個一維數組—IdxOrders。

Dim intMaxOrdersRows = 830
Orders = Form2.PopArray("Orders", _
    intMaxOrdersRows)
IdxOrders = ComputeArrayIndex(Orders, _
    intMaxOrdersRows)

    一維索引數組可以加速二維數組中行的查找,其速度快於在二維數組中掃描所有行,以查找匹配某個條件的值。這是因爲Visual Basic .NET爲它的Array類提供了一個IndexOf共享方法,該方法可返回與一維數組中的某個值相對應的索引。下面的代碼示例顯示了將此方法用於IdxOrders數組以便從Orders數組恢復OrderID和OrderDate列值的語法。該代碼片段還設置了OrderDate列值的格式,以排除datetime值的不相關時間段。

Dim intIdx As Integer = _
    Array.IndexOf(IdxOrders, _
    Integer.Parse(TextBox1.Text))
Console.WriteLine("{0}, {1}", _
    Orders(intIdx, 0), _
    DateTime.Parse( _
    Orders(intIdx, 3)).ToString("M/dd/yy"))

圖5

    圖5顯示圖4中出現的OrderID值在Button2_Click過程中的最終輸出。請注意,此過程執行對ProductID值的查詢,並改爲顯示ProductName列值。基於ProductID列值恢復ProductName列值的查找邏輯,是本文第二個示例中基於ReportsTo列值查找LastName列值的代碼以及上述代碼片段的擴展。將UnitPrice的格式設置爲貨幣值的方法只需調用常見的FormatCurrency函數。雖然您可以使用更爲可靠的方法來設置貨幣值的格式,但知道Visual Basic .NET支持常見且易於使用的FormatCurrency函數也是很好的。

小結

    對於對遠程數據源的數據訪問來說,DataReader是一種快速、靈活且強大的工具。本文突出說明了.NET應用程序中DataReader的3個特定類型的應用程序,實際上還有許多其他的應用程序。在您的自定義解決方案中使用DataReader可以使這些方案運行得更快,甚至還可能會加強您的.NET基本開發技能。通過將DataReader與數組協同使用,您通常能夠從其獲得附加價值。

posted on 2005年02月25日 10:52 AM

href="http://editblog.csdn.net/msdncolumn/Services/Pingback.aspx" rel="pingback"/>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章