DataSet 是通过 ADO.NET 在前端操作数据时最重要的一个类。在这个版本中,它有许多的新增与改良,此处我们仅能针对重点稍加介绍。
12.9.1 对索引引擎的增强
DataSet 可谓前端应用程序在存储器中的小型数据库,类自身有创建约束(Constraint)与索引的功能,这对于大量数据的过滤查找相当重要。而索引是有序的数据结构,因此排序性能是对大量数据创建索引时需要考虑的。ADO.NET 重新编写了它的排序引擎,可以有效提升排序的性能。
在此,我们所设计的范例程序画面如图12-16 所示:

图12-16 通过替数据表增加 50 万条记录来测试 DataSet 新的索引引擎
我们故意设置数据字段的约束,并在该字段塞入大量随机数,以此测试排序的性能。程序代码范例如列表12-19:
程序代码列表12-19 通过添加包含随机数的数据记录测试 DataTable 排序数据的能力
Dim ds As New DataSet
Dim rand As New Random
Dim timBegin As DateTime
Dim i As Integer, dr As DataRow, iRand As Integer, iMax As Integer
ds.Tables.Add("Tbl1")
ds.Tables(0).Columns.Add("ID", Type.GetType("System.Int32"))
'故意设置 unique 限制,让 DS 要做排序的操作
ds.Tables(0).Columns("ID").Unique = True
ds.Tables(0).Columns.Add("Value", Type.GetType("System.Int32"))
iMax = Integer.Parse(TextBox1.Text)
Label2.Text = "开始插入记录"
Me.Cursor = Cursors.WaitCursor
Me.Refresh()
timBegin = DateTime.Now()
For i = 1 To iMax
'大量重复插入随机数记录,因为该字段定义有 Key
'因此需要排序
Try
iRand = rand.Next(1, iMax * 10)
dr = ds.Tables(0).NewRow
'故意插入随机数,强迫执行排序的操作
dr("ID") = iRand
dr("Value") = iRand
ds.Tables(0).Rows.Add(dr)
Catch ex As Exception
End Try
If i Mod 1000 = 0 Then Label2.Text = i.ToString()
Application.DoEvents()
Next
Me.Cursor = Cursors.Default
Label2.Text = "实际记录条数:" & ds.Tables(0).Rows.Count.ToString() & _
" 时间:" & DateDiff(DateInterval.Second, timBegin, DateTime.Now) & " 秒"
注意,不要直接在开发环境以 Debug 模式测试以上范例,因为调试环境自身会耗掉非常多的时间。另外还值得注意的是,加入的数据数量不可以太小,这样效果会比较明显。你可以将上述的程序代码在 ADO.NET 1.1 和 2.0 中环境都编译后执行一遍,此时可以发现 ADO.NET 2.0 随着数据量增加时,其排序所耗的时间是线性递增的,但 1.1 版在数据条数变多时,会倍增所花的时间。
12.9.2 二进制序列化数据
为了更有效地序列化 DataSet 或 DataTable 实例,ADO.NET 2.0 为这些类新增了一个RemotingFormat 属性,让你可选择以二进制格式序列化 DataSet 或 DataTable 的内容,以提供较小的序列化结果,让你在传输或存储 DataSet 或 DataTable 时有较佳的性能。
我们提供简单程序代码范例如列表12-20:
程序代码列表12-20 完全以二进制格式序列化数据以提升性能
Dim ds As New DataSet
Dim da As New SqlDataAdapter(txtSql.Text, _
ConfigurationSettings.ConnectionStrings("AWConnectionString").ConnectionString)
da.Fill(ds)
Dim bf As New BinaryFormatter
Dim fs As New FileStream(txtPath.Text, FileMode.Create)
' ADO.NET 2.0 新增的 RemotingFormat 属性,设置在序列化时是否完全采用二进制的数据格式
'而没有任何 XML 的描述
If CheckBox1.Checked Then ds.RemotingFormat = SerializationFormat.Binary
bf.Serialize(fs, ds)
fs.Close()
首先我们沿用旧的设置,虽然在范例程序代码中设置 BinaryFormatter 类来序列化 DataSet 类实例,但其内容还是含有大量的 XML 数据,其执行结果如图12-17所示:

图12-17 BinaryFormatter 类仍是以 XML 格式序列化 DataSet 数据的结果
但若设置 RemotingFormat 属性为 SerializationFormat.Binary(默认是SerializationFormat.XML),则在序列化时完全采用二进制的数据格式,如此数据较小,因而较有效率。相同的程序代码,只是多了一项属性设置,其执行结果如图12-18 所示:
在上述范例程序代码中,我们以相同的数据测试,存储在硬盘上的数据若以 XML 格式,大小为 364 Kbytes,改成二进制格式后,大小变成 121 Kbytes。因此当你不需要与其他平台沟通时,纯以 ADO.NET 2.0 来交换数据(若是与远端系统交换数据需要采用 Remoting, 而非 XML 格式的 Web Services),为了性能可以考虑用二进制的格式存储或传递。

图12-18 以二进制格式序列化DataSet数据的结果
12.9.3 DataView的ToTable方法
System.Data.DataView 类实例新增了 ToTable 方法,可以让你轻易地将某个 DataTable 通过设置不同的查看方式后,再转成一个新的 DataTable 实例来使用。我们提供的简单范例画面如图12-19所示:

图12-19 通过 DataView 的 ToTable 函数创建不同字段的新 DataTable
在范例中,你可以鼠标点选上方 DataGridView 的字段名称,我们将曾经点选过的字段列表自动转成新的 DataTable 实例,当作下方 DataGridView 的数据源,程序代码范例如列表12-21所示:
程序代码列表12-21 为用户点选的数据字段另建一个 DataTable 实例并加入该字段
Private Sub DataGridView1_ColumnHeaderMouseClick(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs) Handles _
DataGridView1.ColumnHeaderMouseClick
'将用户点选的字段另组成一个显示数据的 DataTable
If strColLists = "" Then
strColLists = DataGridView1.Columns(e.ColumnIndex).Name()
Else
If strColLists.IndexOf(DataGridView1.Columns(e.ColumnIndex).Name()) >= 0 Then
MessageBox.Show("该字段已经点选过,请勿重复点选", "字段重复", _
MessageBoxButtons.OK, MessageBoxIcon.Information)
Return
End If
strColLists += "," & DataGridView1.Columns(e.ColumnIndex).Name()
End If
'通过 ToTable 方法可以选择某些字段后,再将结果输出成另一个 Table
DataGridView2.DataSource = ds.Tables(0).DefaultView.ToTable(False, _
strColLists.Split(","))
End Sub
在程序代码中,我们通过字符串变量记录用户曾经点选的字段名称,再以字符串类实例所提供的 Split 函数将逗号分隔的字符串转成数组,赋予给 ToTable 函数便可返回数组中所指定字段的新数据表,我们以此为数据源赋予给下方的 DataGridView 控制项。
另外,当你点选下方 DataGridView 的字段名称时,我们再将该字段名称从原有字段名称列表中删除,并重新创建另一个 DataTable 当作下方 DataGridView 的数据源。程序代码范例如列表12-22所示:
程序代码列表12-22 为用户点选的数据字段另建一个 DataTable 实例,并删除该字段
'当用户点选某个字段时,将该字段从 DataTable 既有的字段中删除
Dim strName As String = DataGridView2.Columns(e.ColumnIndex).Name
Dim iPos As Integer = strColLists.IndexOf(strName)
If iPos = 0 Then
‘若是用户点选第一栏,则从字符串去掉字段名称后,还需要去掉逗号
strColLists = strColLists.Substring(strName.Length)
If strColLists.Length > 0 AndAlso strColLists(0) = "," _
Then strColLists = strColLists.Substring(1)
Else
strColLists = strColLists.Substring(0, iPos - 1) & _
strColLists.Substring(iPos + strName.Length)
End If
If strColLists = "" Then
DataGridView2.DataSource = Nothing
Else
DataGridView2.DataSource = ds.Tables(0).DefaultView.ToTable(False, _
strColLists.Split(","))
End If
在程序代码中,我们判断用户点选了哪个字段,将该字段从字符串中删除后还需要判断整个字符串以逗号分隔的格式是否正确。
12.9.4 加强 DataTable 类的功能
在 ADO.NET 1.* 时,离线的数据访问模型以 DataSet 对象为主,因此若要将 XML 的数据装载到 DataTable,必须通过 DataSet 来实现。若我们仅操作一个数据表,不需要访问多个数据表,则还需经过 DataSet 类才能赋予 DataTable 数据或将数据输出成 XML 文件,其过程有点繁琐。ADO.NET 2.0 的 DataTable 类则新增了与 DataSet 相同的 ReadXML、ReadXMLSchema、WriteXML 以及WriteXMLSchema 等方法。因此我们可以直接操作 DataTable 实例,而不需先创建 DataSet 类的实例来赋予 DataTable 实例数据,然后只使用 DataTable 实例。
直接操作 DataTable 类的简单范例程序画面如图12-20所示:
在范例中我们同时提供 DataTable 各项功能的示范,例如将数据从 SQL Server 装载到 DataTable,再通过 DataGridView 编辑 DataTable 内的数据,装载其他数据源的数据内容后,通过对比主键的值,对相等的记录进行合并的操作,重新更新回 SQL Server,以 XML 格式查看 DataTable 内新旧数据的存储方式,或是将数据直接以 XML 格式存放到文件中,再从文件装载到 DataTable 实例,然后与当前的 DataTable 进行合并的操作。

图12-20 测试 DataTable 类的各项功能
直接装载数据到 DataTable 实例
范例执行前我们先在 AdventureWorks 数据库加入一个修改数据的存储过程,内容如程序代码列表12-23所示:
程序代码列表12-23 提供给自行编写的T-SQL 调用或是 SqlDataAdapter 实例调用的存储过程
USE AdventureWorks
GO
CREATE PROC spUpdateCustomer @ID INT,@SalesID INT,@SalesOld INT=0
AS
IF @SalesOld=0
--不管是否有其他人在取出数据后更新过该条记录,一律盖过去
UPDATE Sales.Customer SET SalesPersonID=@SalesID,ModifiedDate=GetDate()
WHERE CustomerID=@ID
ELSE
--避免数据取出之后已经被他人更新过
UPDATE Sales.Customer SET SalesPersonID=@SalesID,ModifiedDate=GetDate()
WHERE CustomerID=@ID AND SalesPersonID=@SalesOld
以往若要将数据库中的数据装载到 DataTable,需要先通过 SqlDataAdapter 类实例将记录放入到 DataSet 实例中,再从 DataSet 实例中取出 DataTable 的对象引用。但我们实际可能需要的仅是在单一 DataTable 实例内操作数据,并不需要多个数据表间的关连,也不需要使用 DataSet 类实例。
现在 ADO.NET 2.0 可以通过新增的 Load 方法轻易地将 DataReader 类的数据放入到 DataTable 实例中。范例程序如程序代码列表12-24所示:
程序代码列表12-24 DataTable 实例通过 Load 方法与 SqlDataReader 类实例直接将 SQL Server 的数据装载到数据表
'从 SQL Server 直接将数据装载到 DataTable
Using cnn As New SqlConnection( _
ConfigurationSettings.ConnectionStrings("AWConnectionString").ConnectionString)
Using cmd As New SqlCommand( _
"SELECT CustomerID,SalesPersonID,ModifiedDate FROM Sales.Customer WHERE CustomerId<=10", cnn)
cnn.Open()
Dim dr As SqlDataReader = cmd.ExecuteReader()
Dim dt As New DataTable
'装载数据
dt.Load(dr)
'替数据表定义主键(Primary Key)约束
dt.Constraints.Add(New System.Data.UniqueConstraint("Pk", _
New System.Data.DataColumn() {dt.Columns(0)}, True))
DataGridView1.DataSource = dt
'只允许通过 DataGridView 修改数据,不允许添加和删除
DataGridView1.AllowUserToAddRows = False
DataGridView1.AllowUserToDeleteRows = False
'PK 不允许编辑
DataGridView1.Columns(0).ReadOnly = True
'修改的日期我们直接在存储过程填入,也不许编辑
DataGridView1.Columns(2).ReadOnly = True
DataGridView1.Columns(2).Width = 200
StatusStrip1.Items(0).Text = "数据取得完毕"
End Using
End Using
在上述范例中我们通过 SqlDataReader 实例取回数据后,以 DataTable 的实例直接调用 Load 来装载,并设置第一栏为主键(Primary Key)。在此用到了 .NET Framework 2.0 新提供的 DataGridView 控制项,它的功能比以往的 DataGrid 控制项丰富得多,因此可以更精细地设置用户通过 DataGridView 访问数据的方式。
通过 SqlDataAdapter 类实例将 DataTable 内的记录更新回数据源
接下来我们以 SqlDataAdapter 类实例将用户通过 DataGridView 更新的记录,直接调用上述程序代码列表12- 23 所编写的存储过程,以此修改服务器上数据库的数据表内的记录,范例如程序代码表12-25所示。
程序代码列表12-25 调用先前编写的存储过程,并通过 SqlDataAdapter 实例将多条记录一起更新回数据库
'通过 SqlDataAdapter 将多条记录一起更新回数据库
Using cnn As New SqlConnection(ConfigurationSettings.ConnectionStrings("AWConnectionString").ConnectionString)
Using cmd As New SqlCommand("spUpdateCustomer", cnn)
cnn.Open()
With cmd
.CommandType = CommandType.StoredProcedure
.Parameters.Add(New SqlParameter("@ID", SqlDbType.Int, 0, _
ParameterDirection.Input, 0, 0, "CustomerID", DataRowVersion.Original, False, Nothing, "", "", ""))
'通过旧记录数据的过滤可以做到多人同时访问数据的一致性
'避免在离线编辑数据时,两个以上的人同时修改到同一条记录,后存结果回去的人
'不晓得先前已经有人编辑过该条记录,导致以他编辑的结果直接覆盖掉前人曾经做
'的修改。
.Parameters.Add(New SqlParameter("@SalesID", SqlDbType.Int, 0, _
ParameterDirection.Input, 0, 0, "SalesPersonID", DataRowVersion.Current, _
False, Nothing, "", "", ""))
.Parameters.Add(New SqlParameter("@SalesOld", SqlDbType.Int, 0, _
ParameterDirection.Input, 0, 0, "SalesPersonID", DataRowVersion.Original, _
False, Nothing, "", "", ""))
End With
Using adp As New SqlDataAdapter
Dim i As Integer
adp.UpdateCommand = cmd
'当发现某条记录已经遭他人修改时,是立刻停止更新其后的记录,还是
'继续更新
adp.ContinueUpdateOnError = CheckBox1.Checked
i = adp.Update(CType(DataGridView1.DataSource, DataTable))
StatusStrip1.Items(0).Text = "数据更新完毕 " & i.ToString() & " 条记录受影响"
End Using
End Using
End Using
在上述的程序代码中,我们还示范了 SqlDataAdapter 类对多人同时访问相同数据表时,如何进行更新数据冲突的处理。默认它会将所有记录的旧数据当作 Update 语法的 Where 条件,所以我们在编写程序代码列表12-23 的存储过程时,就提供了输入旧值的参数。当有两个人先后更新了同一条记录后,后更新者因为 Where 条件限制而更新不到记录。此时 SqlDataAdapter 发现执行完 Update 语法后 SQL Server 返回的受影响记录条数却是 0(若没有其他人更改数据,则数据更新受影响条数最起码为 1),便认为是数据更新冲突,这时就看你设置要 SqlDataAdapter 如何处理该错误。
在图12-20的范例访问中,我们将第一条记录从原有的 281 改成 283,若要在更新时带着该条记录的旧数据,需要服务器端的存储过程、客户端应用程序的设置,通过 DataTable 对各条记录的版本控制,SqlDataAdapter 才可以正确地赋予各参数数据。因此通过 SQL Profiler 可以录制到如图12-21的更新方式:
我们将 DataTable 实例的内容以 XML DiffGram 格式显示出来时,其结构如表12-1所示:

图12-21 更新记录时通过新旧数据的对比来处理多人离线编辑可能发生的数据冲突
表12-1 DataTable 通过 XML DiffGram 来记录其内数据所发生的变化
<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
<DocumentElement>
<Sales.Customer diffgr:id="Sales.Customer1" msdata:rowOrder="0" diffgr:hasChanges="modified">
<CustomerID>1</CustomerID>
<SalesPersonID>283</SalesPersonID>
<ModifiedDate>2005-03-11T21:41:53.2530000-08:00</ModifiedDate>
</Sales.Customer>
<Sales.Customer diffgr:id="Sales.Customer2" msdata:rowOrder="1">
<CustomerID>2</CustomerID>
<SalesPersonID>283</SalesPersonID>
<ModifiedDate>2005-03-11T21:41:53.2630000-08:00</ModifiedDate>
</Sales.Customer>
...
</DocumentElement>
<diffgr:before>
<Sales.Customer diffgr:id="Sales.Customer1" msdata:rowOrder="0">
<CustomerID>1</CustomerID>
<SalesPersonID>281</SalesPersonID>
<ModifiedDate>2005-03-11T21:41:53.2530000-08:00</ModifiedDate>
</Sales.Customer>
</diffgr:before>
</diffgr:diffgram>
通过表12-1可以看到数据表实例有各条记录的修改过程中的数据,除非你调用 AcceptChanges 或 RejectChanges 方法才会清掉这些数据改变的过往记录。
DataTable 类对 XML 的支持
微软在各个数据运行环节中都大幅增强对 XML 数据的访问,ADO.NET 2.0 自然也对 XML 提供更广泛的功能。不管是 SQL Server 2005 的本机 XML 数据类型、多 Schema 与命名空间的支持、增强从 XML 推测 Schema 的引擎(XSD Schema inference engine)等基础功能,还是让 DataTable 类支持 ReadXML、ReadXMLSchema、WriteXML 以及 WriteXMLSchema 等方法。接着,我们测试一下 DataTable 所提供 WriteXml 方,范例程序如程序代码列表12-26所示:
程序代码列表12-26 将 DataTable 内的数据以 DiffGram 选项输出成 XML 数据
'将 DataTable 的内容以 XML 涵盖 Diffgram 的方式显示出来
Dim ba(10000) As Byte
Dim ms As New MemoryStream(ba)
Dim sw As New StreamWriter(ms)
Dim txt As New TextBox
txt.Multiline = True
txt.Dock = DockStyle.Fill
txt.ScrollBars = ScrollBars.Both
Dim dt As DataTable = CType(DataGridView1.DataSource, DataTable)
dt.TableName = "Sales.Customer"
dt.WriteXml(sw, XmlWriteMode.DiffGram, True)
txt.Text = Encoding.UTF8.GetString(ba)
'直接创建一个新的 Form 类实例来显示文字内容
Dim frm As New Form
frm.Controls.Add(txt)
frm.Text = DateTime.Now().ToString & " 创建"

