数据集DataSet
DataSet 由表、关系和约束的集合组成。在 ADO.NET 中,DataTable 对象用于表示 DataSet 中的表。DataTable 表示一个内存内关系数据的表;数据对于所处的基于 .NET 的应用程序来说是本地数据,但可从数据源(例如,使用 DataAdapter 的 Microsoft SQL Server)中导入。
DataSet 类是数据的脱机容器。它不 包含数据库连接的概念,实际上存储在 DataSet 中的数据不一定来源于数据库,它可以是 CSV 文 件中的记录,或是来自测量设备中的点读取。
数据集由一组数据表组成,每个表都有一些数据列和数据行。除了定义数据外,还 可以在 DataSet 中定义表之间的链接。例如,我们常常要定义父 / 子 关系 ( 通常也称为主 / 从关系 ) 。 表中的一个记录 ( 即 Order) 链接到另一 个表的许多记录上 ( 即 Order _ Details) , 这种关系可以在 DataSet 中定义和导航,如图 21-4 所 示。
图 21-4
下一节描述和 DataSet 一起使用的类。
数据表DataTable
DataTable 类是 .NET Framework 类库中 System.Data 命名空间的成员。您可以独立创建和使用 DataTable,也可以作为 DataSet 的成员创建和使用,而且 DataTable 对象也可以与其他 .NET Framework 对象(包括 DataView)一起使用。您可以通过 DataSet 对象的 Tables 属性来访问 DataSet 中表的集合。
数据表非常类似于物理数据库表,它由一些带有特定属性的列组成,可能包含 0 行 或多行数据。数据表也可以定义主键码 ( 可以是一个列或多个列 ) , 列上也可以包含约束。这些信息在本章的其他部分称为“模式”。
为数据表定义模式有几种方式 ( 把 数据集当作一个整体 ) ,这些在介绍了数据列和数据行后讨论。图 21-5 显 示了一些可通过数据表访问的对象。
图 21-5
DataTable 对象 ( 和 DataColumn) 可 以附带任意多个扩展属性。这个集合可以用附属于对象的用户自定义信息来填充。例如,某个列有一个输入掩码,用于验证列的内容是否有效,比较规范的示例是 US 社 会安全号。当数据在中间层中构造,要返回给客户机,进行某些处理时,最适合使用扩展的属性。例如,可以在扩展的属性 ( 如 min 和 max) 中 存储数字列的有效性标准,在验证用户输入时在 UI 层使用它们。
填充数据表时,可以从数据库中选择数据,从文件中读取数据,或在代码中手工填 充, Rows 集合会包含这些检索出来的数据。
Columns 集合包含已经添加到表中的 DataColumn 实 例,它们定义了数据的模式,例如数据类型、是否可为空和默认值等。 Constraints 集合可以 用惟一或主键码约束来填充。
数据表使用模式信息的一个示例是在 DataGrid( 详 见第 22 章 ) 中显示数据。 DataGrid 控 件使用属性 ( 例如列的数据类型 ) 来确定该列 应使用什么控件。数据库中的 bit 列在 DataGrid 中 显示为一个复选框。如果列在数据库模式中定义为 NOT NULL ,那么该列就存储在 DataColumn 中, 以便在用户试图移出数据行时测试该列。
表的架构或结构由列和约束表示。使用 DataColumn 对象以及 ForeignKeyConstraint 和 UniqueConstraint 对象定义 DataTable 的架构。表中的列可以映射到数据源中的列、包含从表达式计算所得的值、自动递增它们的值,或包含主键值。
数据列 DataColumn
DataColumn 对 象定义了数据表中某列的属性,例如该列的数据类型,该列是否为只读,以及其他属性。列可以在代码中创建,或者由运行库自动生成。
在创建一个列时,给它指定名称是很有用的,否则运行库就会为该列生成一个名称,其格式是 Columnn , 其 中 n 是一个递增的数字。
列的数据类型可以在构造函数中提供,也可以通过设置 DataType 属性来指 定。把数据加载到数据表中后,就不能改变列的数据类型了,否则会抛出 ArgumentException 异常。
创建的数据列可以包含表 21-3 所示的 .NET Framework 数据类型。
表 21-3
Boolean | Decimal | Int64 | TimeSpan |
Byte | Double | Sbyte | UInt16 |
Char | Int16 | Single | UInt32 |
DateTime | Int32 | String | UInt64 |
一旦创建好,就要给 DataColumn 对 象设置其他属性,例如该列是否可为空或者设置默认值。下面的代码段显示了给 DataColumn 设 置的一些常见选项:
DataColumn customerID = new DataColumn("CustomerID" , typeof(int));
customerID.AllowDBNull = false;
customerID.ReadOnly = false;
customerID.AutoIncrement = true;
customerID.AutoIncrementSeed = 1000;
DataColumn name = new DataColumn("Name" , typeof(string));
name.AllowDBNull = false;
name.Unique = true;
可以给 DataColumn 设置如表 21-4 所 示的属性。
表 21-4
属 性 | 说 明 |
AllowDBNull | 如果为 true , 该列就可以设置为 DBNull |
AutoIncrement | 定义自动生成的列值为一个递增的数字 |
AutoIncrementSeed | 定义 AutoIncrement 列 最初的种子值 |
AutoIncrementStep | 用默认的步骤定义自动生成列值的步骤 |
Caption | 可以用于在屏幕上显示列名 |
ColumnMapping | 指定当 DataSet 通过调用 DataSet.WriteXml 来 保存时,列如何映射到 XML 上 |
ColumnName | 列名。如果没有在构造函数中设置,就由运行库自动 生成 |
DataType | 列的 System.Type 值 |
DefaultValue | 可以定义列的默认值 |
Expression | 该属性定义表达式用于所计算的列 |
数据行 DataRow
这个类构成了 DataTable 类 的另一部分。数据表中的列根据 DataTable 类来定义 , 表 中的实际数据用 DataRow 对象来访问。下面的示例说明了如何访问数据表中的行。首先是连接:
string source = "server=(local)//NetSDK;" +
"uid=QSUser;pwd=QSPassword;" +
"database=northwind";
string select = "SELECT ContactName,CompanyName FROM Customers";
SqlConnection conn = new SqlConnection(source);
下面的代码显示了 SqlDataAdapter 类,它用于选择 DataSet 中 的数据。 SqlDataAdapter 使用 SQL 子 句,在 DataSet 中用下面查询的结果填写表 Customers 。 将在 21.7 节中进一步讨论 SqlDataAdapter 类。
SqlDataAdapter da = new SqlDataAdapter(select, conn);
DataSet ds = new DataSet();
da.Fill(ds , "Customers");
在下面的代码中注意 , 使用 DataRow 的 索引器访问数据行上的值。给定列的值可以用几个重载的索引器来检索,这样就可以通过已知的列号、列名或 DataColum 来 检索数据的值:
foreach(DataRow row in ds.Tables["Customers"].Rows)
Console.WriteLine("'{0}' from {1}" , row[0] ,row[1]);
DataRow 最吸引人的一个方面就是它的版本功能。 DataRow 可 以接收某一行上指定列的各个值,其版本见表 21-5 。
表 21-5
DataRow 的 Version 值 | 说 明 |
Current | 列中目前存在的值,如果没有进行编辑,该值与初值 相同。如果进行了编辑,该值就是最后输入的一个有效值 |
Default | 默认值 ( 列 的任何默认设置 ) |
Original | 最初从数据库中选择出来的列值。如果调用了 DataRow 的 AcceptChanges 方 法,该值就更新为当前值 |
Proposed | 对列进行逐步的修改时,可以检索到这个已改变的 值。如果在行上调用了方法 BeginEdit() ,并进行了修改,每一列都会有一个推荐值,直到调 用了 EndEdit() 或 CancelEdit() 为 止 |
可以以许多方式使用给定列的版本。例如,在数据库中更新数据行时,常常使用如下 SQL 语 句:
UPDATE Products
SET Name = Column.Current
WHERE ProductID = xxx
AND Name = Column.Original;
显然,这段代码永远不会编译,但它说明了列的初值和当前值的一个用法。
要从 DataRow 索引器中检索某个版本的值,应使用索引器方法,把 DataRowVersion 值 作为一个参数。下面的代码段说明了如何获得 DataTable 中每一列的所有值:
foreach (DataRow row in ds.Tables["Customers"].Rows )
{
foreach ( DataColumn dc in ds.Tables["Customers"].Columns )
{
Console.WriteLine ("{0} Current = {1}" , dc.ColumnName ,
row[dc,DataRowVersion.Current]);
Console.WriteLine (" Default = {0}" , row[dc,DataRowVersion.Default]);
Console.WriteLine (" Original = {0}" , row[dc,DataRowVersion.Original]);
}
}
整个数据行有一个状态标志 RowState ,可以用于确定在返回数据库时需要对 该行进行什么操作。 RowState 标志跟踪对 DataTable 所 作的所有改变,例如添加新行、删除现有的行,改变表中的列。当数据与数据库保持一致时,行的状态标志用于确定应执行什么 SQL 操 作。这些标志由 DataRowState 枚举定义,如表 21-6 所 示。
表 21-6
DataRowState 值 | 说 明 |
Added | 把新数据行添加到 DataTable 的 Rows 集 合中。在客户机中创建的所有行都设置为这个值,最终在与数据库保持一致时,会使用 SQL INSERT 语 句 |
Deleted | 通过 DataRow.Delete() 方 法把 DataTable 中的数据行标记为删除。该行仍存在 DataTable 中, 但在屏幕上看不到它 ( 除非显式设置 DataView) 。 DataView 在 下一章讨论。在 DataTable 中标记为已删除的 Rows 将 在与数据库保持一致时从数据库中删除 |
Detached | 数据行在创建后立即显示为这个状态,调用 DataRow.Remove() 也 可以返回这个状态。分立的行不是任何 DataTable 的一部分,因此处于这种状态的行不能使用任 何 SQL 语句 |
Modified | 如果列中的值发生了改变,就会修改一行数据 |
Unchanged | 自从最后一次调用 AcceptChanges 以 来,数据行都没有发生改变 |
行的状态也取决于在其上调用的方法。一般在成功更新数据源 ( 即把改变返回数据库 后 ) 之后调用 AcceptChanges 方 法。
修改 DataRow 中数据最常见的方式是使用索引器,但如果对数据进行了许多修 改,就需要考虑使用 BeginEdit() 和 EndEdit() 方 法。
为 DataTable 创 建模式有 3 种方式:
● 让运行库来完成
● 编写代码来创建表
● 使用 XML 模 式生成器
(1) 运行 库 生成的模式
前面的 DataRow 示例用下面的代码从数据库中选择数据,并生成一个 DataSet :
SqlDataAdapter da = new SqlDataAdapter(select , conn);
DataSet ds = new DataSet();
da.Fill(ds , "Customers");
这是很容易使用的,但也有几个缺点。例如,必须利用默认的列名来处理—— 这是可以的,但在某些情况下,还要把物理数据库的列 ( 如 PKID) 重 新命名为一个用户友好性更高的名称。
自然,可以在 SQL 子 句中重新给列指定别名,例如 SELECT PID AS PersonID FROM PersonTable 。 最好不要在 SQL 中重新给列命名,因为列实际上只需要在屏幕上显示一个“比较好”的名称即可。
自动生成 DataTable/DataColumn 的 另一个潜在的问题是不能控制运行库为列选择的数据类型。运行库可以确定正确的数据类型,但有时需要对此有更多的控制。例如,为给定的列定义枚举类型,以简 化类的用户代码。如果接受运行库生成的默认列类型,该列就可能是一个 32 位的整数,而不是有预定选 项的枚举。
最后,也是最有可能出的问题是,在使用自动生成的表时,不能对 DataTable 中 的数据进行类型安全的访问—— 索引器就会返回 object 的 实例,而不是派生的数据类型。如果要用代码对表达式进行类型转换,就可以跳过下面的章节。
(2) 手工编码的模式
用生成代码来创建 DataTable , 再用相关的 DataColumns 来 填充是相当简单的。本节的示例将访问 Northwind 数据库中的 Product 表 , 如 图 21-6 所示。
图 21-6
下面的代码生成了一个 DataTable ,对应于上面的模式 ( 但 没有包含可为空的列 ) :
public static void ManufactureProductDataTable(DataSet ds)
{
DataTable products = new DataTable("Products");
products.Columns.Add(new DataColumn("ProductID", typeof(int)));
products.Columns.Add(new DataColumn("ProductName", typeof(string)));
products.Columns.Add(new DataColumn("SupplierID", typeof(int)));
products.Columns.Add(new DataColumn("CategoryID", typeof(int)));
products.Columns.Add(new DataColumn("QuantityPerUnit", typeof(string)));
products.Columns.Add(new DataColumn("UnitPrice", typeof(decimal)));
products.Columns.Add(new DataColumn("UnitsInStock", typeof(short)));
products.Columns.Add(new DataColumn("UnitsOnOrder", typeof(short)));
products.Columns.Add(new DataColumn("ReorderLevel", typeof(short)));
products.Columns.Add(new DataColumn("Discontinued", typeof(bool)));
ds.Tables.Add(products);
}
可以改变 DataRow 示例中的代码,使用新生成的表定义:
string source = "server=(local)//NetSDK;" +
"integrated security=sspi;" +
"database=Northwind";
string select = "SELECT * FROM Products";
SqlConnection conn = new SqlConnection(source);
SqlDataAdapter cmd = new SqlDataAdapter(select, conn);
DataSet ds = new DataSet();
ManufactureProductDataTable(ds);
cmd.Fill(ds, "Products");
foreach(DataRow row in ds.Tables["Products"].Rows)
Console.WriteLine("'{0}' from {1}", row[0], row[1]);
ManufactureProductDataTable 方法创建一个新 DataTable , 依次添加每个列,最后把这个表添加到 DataSet 中表的清单上。 DataSet 有 一个索引器,它的参数是表名,给调用者返回该 DataTable 。
上面的示例仍不是类型安全的,因为在列上使用了索引器来检索数据。最好是有一个类 ( 或 一组类 ) 派生于 DataSet 、 DataTable 和 DataRow ,为表、行和列定义类型安全的 存取器。可以自己生成这段代码,这并不是特别乏味,最终将得到可以进行类型安全访问的类。
如果不愿意自己生成这些类型安全的类,可以使用帮助。 .NET Framework 允 许使用 XML 模式来定义 DataSet 、 DataTables 和 本节介绍的其他类。本章后面的 XML 模式一节详细介绍了这个方法。