数据适配器
数据适配器对象充当数据源与DataSet对象间的双向桥梁。DataSet是一种非连接的数据容器,适配器负责对它进行填充,并能把它的数据提交给特定的数据源。
命令与数据适配器最大的不同在于获取数据后的返回方式。查询命令能返回一种只读、只进的游标—数据读取器。数据适配器能执行数据访问,获取所有数据,并将其打包在内存容器中—DataSet或DataTable。其实,数据适配器是一种额外的抽象层,构建于命令/数据读取器对之上。数据适配器在内部会使用命令进行查询,使用数据读取器遍历所有记录,并填充给用户的DataSet。
SqlDataAdapter类
数据适配器类继承于DbDataAdapter类,并实现了IDbDataAdapter和ICloneable接口。
SqlDataAdapter类的属性见下表:
数据适配器是一条双向信道,能够将数据源数据读取到内存表中,也能够将内存中的数据写回数据源中。
IDbAdapter接口的xxxCommand成员用于在更新操作期间控制内存数据写入数据库的方式。SelectCommand有些特殊,它不仅在批量更新中发挥着作用,还是填充操作必不可少的成员。
一旦数据被加载到内存中,.NET程序便可对其执行非连接的客户端更新。批量更新是一种由客户端应用程序触发的数据提供程序过程,会将内存中所有挂起的更改应用到数据源中。在这个过程的执行期间,需要一套针对特定数据库管理系统的命令执行3项基本的操作:插入、更新、删除。InsertCommand、UpdateCommand、DeleteCommand属性存储的SqlCommand对象正是分别针对这些操作的命令。
ADO.NET批量更新包括一系列通过数据适配器顺序提交给数据库的命令。作为开发者,我们只需一条命令便可触发批量更新。从概念上应记住,ADO.NET批量更新不同于通过单条命令提交的一系列查询,也不意味着将批量的命令和数据统统移动到DBMS中,并在那执行。
批量更新功能强大,但并不适用于ASP.NET应用程序。问题在于Web应用程序工作在无状态协议上。这样,如果要使整个框架正常运行,我们要将内存表缓存到会话中,而这不是所有应用程序都能承受的。此外还应注意,使用批量更新可减少大量的编码工作,并可对其进行配置来处理复杂的更新。但在ADO.NET 1.x中,由于每次操作都使用其自己的命令,因而使用批量更新并无多少性能优势。从ADO.NET 2.0开始,我们可以通过新的UpdateBatchSize属性将多个更新操作集中在一条命令中。
SqlDataAdapter的方法见下表:
数据适配器对象使用SelectCommand属性从数据源获取模式和数据。与SelectCommand关联的连接对象不必手动打开。如果该连接在读取执行前处于关闭状态,那么在获取数据时它会被自动打开,读取完毕后自动关闭。如果连接已被手动打开,则会一直保持该状态,直到手动关闭。
数据适配器对象会使用Fill方法将通过查询获得的数据填充到内存对象中。这个内存结构为DataSet或DataTable对象。填充DataSet对象就是填充其中的表。数据适配器可以为程序生成的每个结果集创建一张表,表的映射代码决定其方式(如果某个表已存在,则会被更新)。将结果集映射到个上的过程分为两个阶段,分别为表映射和列映射。第一阶段中,数据适配器会确定DataTable的名称,该对象包含当前结果集记录。每个DataTable被指定的默认名称可由程序员随意更改。
DataTable的默认名称取决于所调用的Fill方法的签名,例如下面的示例:
DataSet ds = new DataSet();
adapter.Fill(ds);
adapter.Fill(ds, “MyTable”);
有两种方法更改表的名称:一是在DataSet被填充后;二是创建表映射。对于后者,我们可以通过设置将其他名称映射到要更改名称的表上。为在数据适配器上定义表映射,要用于TableMappings属性。
我们还可以使用Fill方法来填充DataTable,在这种情况下,只会使用第一个结果集,并且只会执行一种映射,即列映射。
DataTable dt = new DataTable();
adapter.Fill(dt);
加载设置
在ADO.NET 2.0和更高版本中,在填充操作执行期间,可用FillLoadOption属性指定数据加载到各种数据表的方式。FillLoadOption属性可接受LoadOption枚举值。下表列出了各种取值的含义:
对于每种情况,仅当输入行的主键与现有行的主键匹配时,所指定的行为才会生效。
OverwriteChanges可满足我们用新数据对表进行初始化的需要。PreserveChanges适合于对现有内存数据与当前数据库状态进行同步。如果要更新DataSet中代表数据库读取的原始数据的值,可使用Upsert。
内存中的行要维护两种值:当前值和原始值。当前值为读取单元格时得到的值,而原始值是最后一次提交时存储在单元格中的值。向一个新插入的行赋值时,设置的是当前值,原始值得为空(null)。被赋予的值在提交给数据库后才能真正在数据库中生效。为提交某行,可调用AcceptChanges方法,调用该方法后,当前值会被复制并覆盖原始值,该行的状态变为未更改,不再有挂起的更改。
提交某行(DataRow类)的更改给原始版数据:
row.AcceptChanges();
读取某行的当前值:
row[“firstname”].ToString();
读取某行的原始值:
row[“firstname”, DataRowVersion.Original].ToString();
表映射机制
对于查询生成的结果集,.NET数据提供程序会为其赋予一个默认的名称。适配器会在TableMappings查找与正在读取的结果集默认名称匹配的数据项。如果找到匹配项,数据适配器会读取被映射的名称。接着,通过在映射中指定的名称,它会试图确定DataSet中DataTable对象的位置。
TableMappings属性代表DataTableMappingCollection类型的集合对象,每个数据项为DataTableMapping对象,它代表一对名称:源表名称和内存表名称。
示例代码:
DataSet ds = new DataSet();
DataTableMapping dtm1, dtm2, dtm3;
dtm1 = adapter.TableMappings.Add(“Table”, “Employees”);
dtm2 = adapter.TableMappings.Add(“Table1”, “Products”);
dtm3 = adapter.TableMappings.Add(“Table2”, “Orders”);
adapter.Fill(ds);
如果只是想为DataSet表赋予一个容易记忆的名称,可使用这样的代码:
DataSet ds = new DataSet();
adapter.Fill(ds);
ds.Tables[“Table”].TableName = “Employees”;
ds.Tables[“Table1”].TableName = “Products”;
示例代码:
DataSet ds = new DataSet();
DataTableMapping dtm1;
dtm1 = adapter.TableMappings.Add(“Table”, “Employees”);
dtm1.ColumnMappings.Add(“employeeid”, “ID”);
dtm1.ColumnMappings.Add(“firstname”, “Name”);
dtm1.ColumnMappings.Add(“lastname”, “FamilyName”);
adapter.Fill(ds);
当数据适配器在收集要填充DataSet的数据时,缺少映射操作会在两种情况下执行,分别为:无法在TableMappings集合中找到默认的名称时;ColumnMappings集合中某个列名不存在时。为处理这两个异常,要通过MissingMappingAction属性对数据适配器的行为进行定义。该属性是MissingMappingAction枚举类型,其定义见下表:
在表映射阶段,如果无法确定DataSet中表的名称或DataSet表不包含期望映射名称的列,则需要执行缺少模式操作。MissingSchemaAction属性用于指定缺少表模式时要执行的操作。该属性是MissingSchemaAction枚举类型,其定义见下表:
MissingMappingAction与MissingSchemaAction不会有真正的异常开销,但仍影响代码的效率。如果正好需要以某种固定的模式重复填充某个空的DataSet,则可以使用预填充模式信息的DataSet对象。FillSchema方法能确保所有需要的对象都事先创建。
DataTable[] FillSchema(DataSet ds, SchemaType mappingMode);
该方法接受一个DataSet,其中添加了与适配器关联查询命令所需的表。该方法会返回创建的所有DataTable对象(其中仅包含模式,而没有数据)。SchemaType是枚举类型,其取值见下表:
批量更新
更新过程中的冲突检测可能会想起相当大的开销,甚至会使批量更新方案的优势大大降低,在数据竞争程度较低的环境下,批量更新才比较有效。
要将客户端更改提交给服务器,可使用数据适配器的Update方法。数据只能以表为单位提交。如果未指定表名,则使用默认名称—Table。如:
adapter.Update(ds, “MyTable”);
Update方法能为特定表中被插入、更新、删除的行逐一准备并执行特殊生成的Insert、Update、Delete语句。该方法返回一个整数表示被成功更新的行数。如果ContinueUpdateOnError设置为true,则某行在更新时出现错误后,更新仍会继续,直到所有行都处理完毕。DataSet中被成功更新的行会被提交并被标记为未更改。
命令生成器
在ADO.NET中,Insert、Update、Delete命令可以自动生成并暴露给数据适配器。命令生成器对象可以完成这些工作,但它不能适用于所有情况,命令的自动生成仅能在特定环境下执行。具体来说,如果表是以多表联接获得的,或表中含有通过计算(或总计)得到的列,命令生成器则无法生成任何命令。仅当适配器执行Update方法时,命令生成器才能发挥作用。那么命令生成器如何为泛化的表生成更新命令呢?这就是SelectCommand属性所要解决的问题。
为了生成各种更新命令,命令生成器会使用SelectCommand来获取所需的元数据。我们必须将SelectCommand设置为包含主键列名称及要操纵列名称的查询字符串,只有这些列会被更新,待更新或删除的行用主键来标识。
数据适配器要通过命令生成器的构造函数与生成器进行关联。如下所示:
SqlCommand cmd = new SqlCommand();
cmd.CommandText = “Select employeeid, lastname From employees”;
cmd.Connection = conn;
adapter.SelectCommand = cmd;
SqlCommandBuilder builder = new SqlCommandBuilder(adapter);
adapter.InsertCommand = builder.GetInsertCommand();
adapter.DeleteCommand = builder.GetDeleteCommand();
adapter.UpdateCommand = builder.GetUpdateCommand();
生成器会请求元数据,并在首次请求时生成并缓存命令。每种命令可通过相应的方法获取,这些方法为GetInsertCommand、GetUpdateCommand、GetDeleteCommand。注意,命令生成器不会自动设置数据适配器的相应命令属性,必须手动设置adapter的相关命令属性。