• 使用 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与数组协同使用,您通常能够从其获得附加价值。

  • 相关阅读:
    改变原数组的filter
    fireEvent2
    Ajaxを勉強しよ
    javascript 地图
    fillZero函数
    window.onerror
    とある要素以下にある textNode で一致する textNode を XPath で高速に取り出す
    判定是否为表单元素
    Django中判断用户是否登陆
    【 如果你和我一样在一栋33层大厦的27层工作,在这栋大厦里发生了火灾,那么你该怎么办? 看看也许会保住你的性命!!!】
  • 原文地址:https://www.cnblogs.com/friendwang1001/p/412896.html
Copyright © 2020-2023  润新知