目录
ADO.NET
ADO.NET 是一组向 .NET Framework 程序员公开数据访问服务的类。 ADO.NET 为创建分布式数据共享应用程序提供了一组丰富的组件。 它提供了对关系数据、XML 和应用程序数据的访问,因此是 .NET Framework 中不可缺少的一部分。 ADO.NET 支持多种开发需求,包括创建由应用程序、工具、语言或 Internet 浏览器使用的前端数据库客户端和中间层业务对象。
.NET Framework 数据提供程序
.NET Framework 数据提供程序用于连接到数据库、执行命令和检索结果。 这些结果将被直接处理,放置在 DataSet
中以便根据需要向用户公开、与多个源中的数据组合,或在层之间进行远程处理。 .NET Framework 数据提供程序是轻量程序,可以在数据源与代码之间创建一个精简层,并在不弱化功能的情况下提高性能。
下表列出了 .NET Framework 中包含的数据提供程序。
.NET Framework data provider — .NET Framework 数据提供程序 | 说明 |
---|---|
用于 MySQL 的 .NET Framework 数据访问接口 | 提供 MySQL 的数据访问。 使用 MySql.Data.MySqlClient 命名空间。 |
.NET Framework 数据提供程序的核心对象
下表概述了构成 .NET Framework 数据提供程序的四个核心对象。
对象 | 说明 |
---|---|
Connection | 建立与特定数据源的连接。 所有 Connection 对象的基类均为 DbConnection 类。 |
Command | 对数据源执行命令。 公开 Parameters ,并可在 Transaction 范围内从 Connection 执行。 所有 Command 对象的基类均为 DbCommand 类。 |
DataReader | 从数据源中读取只进且只读的数据流。 所有 DataReader 对象的基类均为 DbDataReader 类。 |
DataAdapter | 使用数据源填充 DataSet 并解决更新。 所有 DataAdapter 对象的基类均为 DbDataAdapter 类。 |
除了本文档前面的表中所列出的核心类之外,.NET Framework 数据提供程序还包含下表中列出的类。
对象 | 说明 |
---|---|
Transaction | 将命令登记在数据源处的事务中。 所有 Transaction 对象的基类均为 DbTransaction 类。 ADO.NET 还使用 System.Transactions 命名空间中的类提供对事务的支持。 |
CommandBuilder | 一个帮助器对象,它自动生成 DataAdapter 的命令属性或从存储过程中派生参数信息,并填充 Parameters 对象的 Command 集合。 所有 CommandBuilder 对象的基类均为 DbCommandBuilder 类。 |
ConnectionStringBuilder | 一个帮助器对象,它提供一种用于创建和管理由 Connection 对象使用的连接字符串的内容的简单方法。 所有 ConnectionStringBuilder 对象的基类均为 DbConnectionStringBuilder 类。 |
Parameter | 定义命令和存储过程的输入、输出和返回值参数。 所有 Parameter 对象的基类均为 DbParameter 类。 |
Exception | 在数据源中遇到错误时返回。 对于在客户端上遇到的错误,.NET Framework 数据提供程序将引发 .NET Framework 异常。 所有 Exception 对象的基类均为 DbException 类。 |
Error | 公开数据源返回的警告或错误中的信息。 |
ClientPermission | 为 .NET Framework 数据提供程序代码访问安全属性而提供。 所有 ClientPermission 对象的基类均为 DBDataPermission 类。 |
EntityClient 提供程序
EntityClient 提供程序可用来基于实体数据模型 (EDM) 访问数据。 与其他 .NET Framework 数据提供程序不同,该提供程序不直接与数据源进行交互, 而是使用实体 SQL 与基础数据提供程序进行通信。
ADO.NET 中的并行执行
.NET Framework 中的并行执行是指在安装了 .NET Framework 的多个版本的计算机上以独占方式使用编译的应用程序所针对的版本执行该应用程序的能力。
使用 .NET Framework 的一种版本编译的应用程序可以在 .NET Framework 的其他版本上运行。 不过,建议您为安装的每个 .NET Framework 版本都编译一个相应版本的应用程序,并单独运行这些应用程序。 在任一方案中,您都应该知道各版本之间 ADO.NET 中的更改,这些更改可能影响应用程序的向前或向后兼容性。
SqlCommand 执行
在 .NET Framework 1.1 版以及更高版本中,只有当命令含有参数时,ExecuteReader
才会在 sp_executesql 存储过程的上下文中执行该命令,从而提高性能。 因此,如果非参数化命令中包含影响连接状态的命令,会修改在连接打开时执行的所有后续命令的连接状态。
请考虑下面这个在 ExecuteReader
调用中执行的批命令。
SET NOCOUNT ON;
SELECT * FROM dbo.Customers;
NOCOUNT
对连接打开时执行的任何后续命令都将保持为 ON
。
ADO.NET 代码示例
本页面中的代码列表演示如何使用下面的 ADO.NET 技术从数据库中检索数据:
-
ADO.NET 数据提供程序:
-
MySqlClient
(MySql.Data.MySqlClient
)
-
-
ADO.NET 实体框架:
-
LINQ to Entities
-
类型化 ObjectQuery
-
EntityClient
(System.Data.EntityClient
)
-
-
LINQ to SQL
ADO.NET 数据提供程序示例
以下代码列表演示如何使用 ADO.NET 数据提供程序从数据库中检索数据。 数据在一个 DataReader
中返回。
MySqlClient
通过以下代码来连接和加载数据,只需使用 CREATE TABLE
和 INSERT INTO
SQL 语句即可。 代码使用 MySqlConnection
类的以下方法:
-
OpenAsync()
,用于与 MySQL 建立连接。 -
CreateCommand
(),用于设置 CommandText 属性 -
ExecuteNonQueryAsync
(),用于运行数据库命令。
将 Server
、Database
、UserID
、Password
参数替换为你在创建服务器和数据库时指定的值。
using System;
using System.Threading.Tasks;
using MySql.Data.MySqlClient;
namespace MySqlExample
{
class MySqlCreate
{
static async Task Main(string[] args)
{
var builder = new MySqlConnectionStringBuilder
{
Server = "127.0.0.1",
Database = "YOUR-DATABASE",
UserID = "root",
Password = "root",
SslMode = MySqlSslMode.Required,
};
using (var conn = new MySqlConnection(builder.ConnectionString))
{
Console.WriteLine("打开连接");
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
command.CommandText = "DROP TABLE IF EXISTS inventory;";
await command.ExecuteNonQueryAsync();
Console.WriteLine("表是否存在?存在则删除,重新创建");
command.CommandText = "CREATE TABLE inventory (id serial PRIMARY KEY, name VARCHAR(50), quantity INTEGER);";
await command.ExecuteNonQueryAsync();
Console.WriteLine("表创建完成");
command.CommandText = @"INSERT INTO inventory (name, quantity) VALUES (@name1, @quantity1),
(@name2, @quantity2), (@name3, @quantity3);";
command.Parameters.AddWithValue("@name1", "banana");
command.Parameters.AddWithValue("@quantity1", 150);
command.Parameters.AddWithValue("@name2", "orange");
command.Parameters.AddWithValue("@quantity2", 154);
command.Parameters.AddWithValue("@name3", "apple");
command.Parameters.AddWithValue("@quantity3", 100);
int rowCount = await command.ExecuteNonQueryAsync();
Console.WriteLine(String.Format("插入的行数 {0}", rowCount));
}
// 连接将被'using'块关闭
Console.WriteLine("关闭连接");
}
Console.WriteLine("按 RETURN 退出");
Console.ReadLine();
}
}
}
在 ADO.NET 中检索和修改数据
任何数据库应用程序的一项主要功能是连接数据源并检索数据源中包含的数据。 ADO.NET 的 .NET Framework 数据提供程序充当应用程序和数据源之间的桥梁,使你可以执行命令以及使用 DataReader 或 DataAdapter 检索数据。 任何数据库应用程序的一项关键功能是更新数据库中存储的数据的能力。 在 ADO.NET 中,更新数据时会使用 DataAdapter、DataSet
和 Command 对象;此外,还可能会使用事务。
连接到 ADO.NET 中的数据源
在 ADO.NET 中,通过在连接字符串中提供必要的身份验证信息,使用 Connection 对象来连接到某个特定数据源。 你使用的 Connection 对象取决于数据源的类型。
随 .NET Framework 提供的每个 .NET Framework 数据提供程序都具有一个 DbConnection
对象:适用于 OLE DB 的 .NET Framework 数据提供程序包括一个 OleDbConnection
对象,适用于 SQL Server 的 .NET Framework 数据提供程序包括一个 SqlConnection
对象,适用于 ODBC 的 .NET Framework 数据提供程序包括一个 OdbcConnection
对象,适用于 Oracle 的 .NET Framework 数据提供程序包括一个 OracleConnection
对象。
连接到 MySQL 数据源
using System;
using System.Threading.Tasks;
using MySql.Data.MySqlClient;
namespace MySqlExample
{
class MySqlCreate
{
static async Task Main(string[] args)
{
var builder = new MySqlConnectionStringBuilder
{
Server = "127.0.0.1",
Database = "YOUR-DATABASE",
UserID = "root",
Password = "root",
SslMode = MySqlSslMode.Required,
};
using (var conn = new MySqlConnection(builder.ConnectionString))
{
}
}
}
}
在 ADO.NET 中的连接字符串
连接字符串包含作为参数从数据提供程序传递到数据源的初始化信息。 数据提供程序接收连接字符串,作为 DbConnection.ConnectionString
属性的值。 提供程序解析连接字符串,并确保语法正确且支持关键字。 然后 DbConnection.Open()
方法将已解析的连接参数传递到数据源。 数据源执行进一步验证并建立连接。
连接字符串生成器
在 ADO.NET 的早期版本中,不会对具有串联字符串值的连接字符串进行编译时检查,因此在运行时会产生不正确的关键字 ArgumentException
。 每个 .NET Framework
数据提供程序数据提供程序支持的连接字符串关键字的语法不同,这使得手动构造有效连接字符串变得很困难。 为解决这个问题,ADO.NET 2.0 为每个 .NET Framework 数据提供程序引入了新的连接字符串生成器。 每个数据提供程序包括一个从 DbConnectionStringBuilder
继承的强类型连接字符串生成器类。 下表列出了 .NET Framework 中 MySQL 数据提供程序数据提供程序及其关联的连接字符串生成器类。
提供程序 | ConnectionStringBuilder 类 |
---|---|
MySql.Data.MySqlClient | MySql.Data.MySqlClient.MySqlConnectionStringBuilder |
连接字符串注入式攻击
当使用动态字符串串联根据用户输入生成连接字符串时,可能发生连接字符串注入式攻击。 如果未验证字符串并且未转义恶意文本或字符,则攻击者可能会访问服务器上的敏感数据或其他资源。 例如,攻击者可以通过提供分号并追加其他值来发起攻击。 连接字符串通过“last one wins”算法分析,恶意的输入被替换为合法的值。
连接字符串生成器类旨在排除推测,防止出现语法错误和安全漏洞。 它们提供与每个数据提供程序允许的已知键/值对相对应的方法和属性。 每个类都保持一个固定的同义词集合,可以将同义词转换为相应的已知键名。 将执行键/值对的有效性检查,无效对会引发异常。 此外,还会以一种安全方式处理插入的值。
下面的示例演示 MySqlConnectionStringBuilder
如何处理为 Password
设置插入的额外值。
var builder = new MySqlConnectionStringBuilder();
builder["Server"] = "127.0.0.1";
builder["Database"] = "YOUR-DATABASE";
builder["UserID"] = "root";
builder["Password"] = "root;NewValue=Bad";
Console.WriteLine(builder.ConnectionString);
输出结果表明,通过用双引号转义该额外值而不作为新的键/值对将其追加到连接字符串,MySqlConnectionStringBuilder
可以正确处理此额外值。
server=127.0.0.1;database=YOUR-DATABASE;user id=root;password="root;NewValue=Bad"
从配置文件生成连接字符串
如果事先知道连接字符串的某些元素,则可以将其存储在配置文件中,并在运行时检索它们以构造完整连接字符串。 例如,可能事先知道数据库的名称,但不知道服务器的名称。 或者,您可能希望用户在运行时提供用户名和密码,而不能在连接字符串中插入其他值。
连接字符串生成器的一个重载构造函数将 String
作为自变量,这可让你提供部分连接字符串,然后通过用户输入使这部分连接字符串成为完整字符串。 该部分连接字符串可以存储在配置文件中并在运行时进行检索。
备注
System.Configuration
命名空间允许通过编程方式访问配置文件(对 Web 应用程序使用WebConfigurationManager
,对 Windows 应用程序使用ConfigurationManager
)。
示例
此示例演示如何从配置文件中检索部分连接字符串并通过设置 DataSource
的 UserID
、Password
和 SqlConnectionStringBuilder
属性完成该连接字符串。 配置文件定义如下。
<connectionStrings>
<clear/>
<add name="partialConnectString"
connectionString="sslmode=Required;"
providerName="System.Data.MySqlClient" />
</connectionStrings>
备注
必须在项目中设置对
System.Configuration.dll
的引用,才能运行代码。
private static void BuildConnectionString(string server, string userName, string userPassword)
{
//检索名为databaseConnection的部分连接字符串
//从应用程序的app.config配置文件。
ConnectionStringSettings settings = ConfigurationManager.ConnectionStrings["partialConnectString"];
if (null != settings)
{
// 检索部分连接字符串。
string connectString = settings.ConnectionString;
Console.WriteLine("Original: {0}", connectString);
//创建新的MySqlConnectionStringBuilder
//从配置文件中获取部分连接字符串。
MySqlConnectionStringBuilder builder = new MySqlConnectionStringBuilder(connectString);
// 提供附加值。
builder.Server = server;
builder.UserID = userName;
builder.Password = userPassword;
Console.WriteLine("Modified: {0}", builder.ConnectionString);
}
}
DataAdapter 和 DataReader
可以使用 ADO.NET DataReader
从数据库中检索只读、只进的数据流。 查询结果在查询执行时返回,在并存储在客户端的网络缓冲区中,直到使用 DataReader
的 Read
方法对它们发出请求 。 使用 DataReader
可以提高应用程序的性能,原因是它只要数据可用就立即检索数据,并且(默认情况下)一次只在内存中存储一行,减少了系统开销。
DataAdapter
用于从数据源检索数据并填充 DataSet
中的表。 DataAdapter
还可将对 DataSet
所做的更改解析回数据源。 DataAdapter
使用 .NET Framework
数据提供程序的 Connection
对象连接到数据源,并使用 Command
对象从数据源检索数据以及将更改解析回数据源。
DataReader 检索数据
使用 DataReader 检索数据
若要使用 DataReader
检索数据,请创建 Command
对象的实例,然后通过调用 Command.ExecuteReader
创建一个 DataReader
,以便从数据源检索行 。 DataReader
提供未缓冲的数据流,该数据流使过程逻辑可以有效地按顺序处理从数据源中返回的结果。 由于数据不在内存中缓存,所以在检索大量数据时,DataReader 是一种适合的选择。
下面的示例演示如何使用 DataReader
,其中 reader
表示有效的 DataReader
,而 command
表示有效的 Command
对象。
reader = command.ExecuteReader();
使用 DataReader.Read
方法从查询结果中获取行。 通过向 DataReader
传递列的名称或序号,可以访问返回行的每一列。 不过,为了实现最佳性能,DataReader
提供了一系列方法,将使你能够访问其本机数据类型(GetDateTime、GetDouble、GetGuid、GetInt32
等)的列值 。 已知基础数据类型时,如果使用类型化访问器方法,将减少在检索列值时所需的类型转换量。
以下示例循环访问一个 DataReader
对象,并从每个行中返回两个列。
static void HasRows(MySqlConnection connection)
{
using (connection)
{
MySqlCommand command = new MySqlCommand(
"SELECT CategoryID, CategoryName FROM Categories;",
connection);
connection.Open();
MySqlDataReader reader = command.ExecuteReader();
if (reader.HasRows)
{
while (reader.Read())
{
Console.WriteLine("{0}\t{1}", reader.GetInt32(0),
reader.GetString(1));
}
}
else
{
Console.WriteLine("No rows found.");
}
reader.Close();
}
}
关闭 DataReader
每次使用完 DataReader
对象后都应调用 Close
方法 。
如果 Command
包含输出参数或返回值,那么在 DataReader
关闭之前,将无法访问这些值 。
当 DataReader
打开时,该 DataReader
将以独占方式使用 Connection
。 在原始 DataReader
关闭之前,将无法对 Connection
执行任何命令(包括创建另一个 DataReader
) 。
备注
不要在类的 Finalize 方法中对 Connection、DataReader 或任何其他托管对象调用 Close 或 Dispose 。 在终结器中,仅释放类直接拥有的非托管资源。 如果类不拥有任何非托管资源,则不要在类定义中包含 Finalize 方法。
使用 NextResult 检索多个结果集
如果 DataReader
返回多个结果集,请调用 NextResult
方法来按顺序循环访问这些结果集 。 以下示例显示 SqlDataReader
如何使用 ExecuteReader
方法处理两个 SELECT 语句的结果。
static void HasRows(MySqlConnection connection)
{
using (connection)
{
MySqlCommand command = new MySqlCommand(
"SELECT id, name FROM inventory;" +
"SELECT id, name FROM inventory;",
connection);
connection.Open();
MySqlDataReader reader = command.ExecuteReader();
while (reader.HasRows)
{
Console.WriteLine("\t{0}\t{1}", reader.GetName(0),reader.GetName(1));
while (reader.Read())
{
Console.WriteLine("\t{0}\t{1}", reader.GetInt32(0),reader.GetString(1));
}
reader.NextResult();
}
}
}
从 DataReader 中获取架构信息
当 DataReader 打开时,可以使用 GetSchemaTable 方法检索有关当前结果集的架构信息 。 GetSchemaTable 将返回一个填充了行和列的 DataTable
对象,这些行和列包含当前结果集的架构信息。 对于结果集的每一列,DataTable 都包含一行。 架构表的每一列都映射到在结果集的行中返回的列的属性,其中 ColumnName 是属性的名称,而列的值为属性的值。 以下示例为 DataReader 编写架构信息。
static void HasRows(MySqlConnection connection)
{
using (connection)
{
MySqlCommand command = new MySqlCommand(
"SELECT id, name FROM inventory;", connection);
connection.Open();
MySqlDataReader reader = command.ExecuteReader();
DataTable schemaTable = reader.GetSchemaTable();
foreach (DataRow row in schemaTable.Rows)
{
foreach (DataColumn column in schemaTable.Columns)
{
Console.WriteLine(String.Format("{0} = {1}",
column.ColumnName, row[column]));
}
}
}
}
从 DataAdapter 填充数据集
ADO.NET
DataSet
是数据常驻内存的表示形式,可提供与数据源无关的一致关系编程模型。 DataSet
表示整个数据集,其中包含表、约束和表之间的关系。 由于 DataSet
独立于数据源,因此 DataSet
可以包含应用程序本地的数据,也可以包含来自多个数据源的数据。 与现有数据源的交互通过 DataAdapter
来控制。
SelectCommand
的 DataAdapter
属性是一个 Command
对象,用于从数据源中检索数据。
InsertCommand
、 UpdateCommand
、 DeleteCommand
和 DataAdapter
属性是 Command
对象,用于按照对 DataSet
中数据的修改来管理对数据源中数据的更新。
Fill
的 DataAdapter
方法用于使用 DataSet
的 SelectCommand
结果填充 。 Fill
将要填充的 DataSet
、DataAdapter
和 DataTable
对象(或要使用从 DataTable
中返回的行来填充的 SelectCommand
的名称)作为它的参数。
备注
使用
DataAdapter
检索表的全部内容会花费些时间,尤其是在表中有很多行时。 这是因为访问数据库,定位和处理数据,然后将数据传输到客户端是需要很长时间的。 将表中全部内容提取到客户端还会在服务器上锁定所有行。 若要提高性能,您可以使用WHERE
子句使返回客户端的行数大为减少。 还可以通过只显式列出SELECT
语句要求的列减少返回到客户端的数据量。 另一种好的变通方法是以批次检索行(例如一次检索几百行),并且在客户端完成当前批次后只检索下一批次。
Fill
方法使用 DataReader
对象来隐式地返回用于在 DataSet
中创建表的列名称和类型,以及用于填充 DataSet
中的表行的数据。 表和列仅在不存在时才创建;否则, Fill
将使用现有的 DataSet
架构。 列类型根据 ADO.NET 中的数据类型映射中的表创建为 .NET Framework 类型。 除非数据源中存在主键且 DataAdapter
.MissingSchemaAction
设置为 MissingSchemaAction
.AddWithKey
,否则不会创建主键。 如果 Fill
发现某个表存在主键,对于主键列的值与从数据源返回的行的主键列的值匹配的行,将使用数据源中的数据重写 DataSet
中的数据。 如果未找到任何主键,则将数据追加到 DataSet
中的表。 Fill
使用在填充 DataSet
时可能存在的任何映射。
备注
如果
SelectCommand
返回OUTER JOIN
的结果,则DataAdapter
不会为生成的PrimaryKey
设置DataTable
值。 您必须自己定义PrimaryKey
以确保正确解析重复行。
以下代码示例创建了一个 SqlDataAdapter
实例,使用 Microsoft SQL Server SqlConnection
数据库的 Northwind
并使用客户列表填充 DataTable
中的 DataSet
。 向 SqlConnection
构造函数传递的 SQL 语句和 SqlDataAdapter
参数用于创建 SelectCommand
的 SqlDataAdapter
属性。
示例
string queryString = "select * from inventory";
MySqlDataAdapter dataAdapter = new MySqlDataAdapter(queryString,connection);
DataSet customers = new DataSet();
dataAdapter.Fill(customers, "inventory");
foreach (DataRow dataRow in customers.Tables["inventory"].Rows)
{
string id = dataRow["id"].ToString();
string name = dataRow["name"].ToString();
string quantity = dataRow["quantity"].ToString();
Console.WriteLine("id:{0} name:{1} quantity:{2}", id,name,quantity);
}
备注
此示例中所示的代码不显式打开和关闭
Connection
。 如果Fill
方法发现连接尚未打开,它将隐式地打开Connection
正在使用的DataAdapter
。 如果Fill
已打开连接,则它还将在Fill
完成时关闭连接。 当处理单一操作(如Fill
或Update
)时,这可以简化您的代码。 但是,如果您在执行多项需要打开连接的操作,则可以通过以下方式提高应用程序的性能:显式调用Open
的Connection
方法,对数据源执行操作,然后调用Close
的Connection
方法。 应尝试使数据源的连接打开的时间尽可能短,以便释放资源供其他客户端应用程序使用。
多个结果集
如果 DataAdapter
遇到多个结果集,则将在 DataSet
中创建多个表。 这些表的命名方式为默认名称 Table 加上N,N 从 0 开始递增,如以 Table0 为第一个表名,依次类推。 如果以参数形式向 Fill
方法传递表名,则这些表的命名方式为默认名称 TableName 加上N,N 从 0 开始递增,如以 TableName0 为第一个表名,依次类推。
从多个 DataAdapter 填充 DataSet
一个 DataSet
可以与任意数量的 DataAdapter
对象一起使用。 每个 DataAdapter
都可用于填充一个或多个 DataTable
对象并将更新解析回相关数据源。 DataRelation
和 Constraint
对象可以在本地添加到 DataSet
,这样您就可以关联来自不同数据源的数据。 例如, DataSet
可以包含来自 Microsoft SQL Server 数据库、通过 OLE DB 公开的 IBM DB2 数据库以及对 XML 进行流处理的数据源的数据。 一个或多个 DataAdapter
对象可以处理与每个数据源的通信。
示例
以下代码示例从 MySQL 上的 Northwind
数据库填充客户列表,从存储在 Microsoft Access 2000 上的 Northwind
数据库填充订单列表。 已填充的表通过 DataRelation
相关联,这样,客户列表将与相应客户的订单一起显示出来。
MySqlDataAdapter custAdapter = new MySqlDataAdapter("SELECT * FROM Customers", customerConnection);
OleDbDataAdapter ordAdapter = new OleDbDataAdapter("SELECT * FROM Orders", orderConnection);
DataSet customerOrders = new DataSet();
custAdapter.Fill(customerOrders, "Customers");
ordAdapter.Fill(customerOrders, "Orders");
DataRelation relation = customerOrders.Relations.Add("CustOrders",
customerOrders.Tables["Customers"].Columns["CustomerID"],
customerOrders.Tables["Orders"].Columns["CustomerID"]);
foreach (DataRow pRow in customerOrders.Tables["Customers"].Rows)
{
Console.WriteLine(pRow["CustomerID"]);
foreach (DataRow cRow in pRow.GetChildRows(relation))
Console.WriteLine("\t" + cRow["OrderID"]);
}
DataAdapter 参数
DbDataAdapter
具有四个用于从数据源检索数据和更新数据源中数据的属性:
-
SelectCommand
属性返回数据源中的数据; -
InsertCommand
、UpdateCommand
和DeleteCommand
属性用于管理数据源中的更新。
调用 SelectCommand
的 Fill
方法之前必须设置 DataAdapter
属性。
在调用 InsertCommand
的 UpdateCommand
方法之前必须设置 DeleteCommand
、Update
或 DataAdapter
属性,具体取决于对 DataTable
中的数据做了哪些更改。
例如,如果已添加行,在调用 InsertCommand
之前必须设置 Update
。 当 Update
正在处理已插入、已更新或已删除的行时,DataAdapter
将使用相应的 Command
属性来处理该操作。 有关已修改行的当前信息将通过 Command
集合传递到 Parameters
对象。
更新数据源中的行时,调用 UPDATE
语句,该语句使用唯一标识符来标识表中要更新的行。 该唯一标识符通常是主键字段的值。 UPDATE 语句使用的参数既包含唯一标识符又包含要更新的列和值,如下面的 Transact-SQL 语句所示。
UPDATE Customers SET CompanyName = @CompanyName
WHERE CustomerID = @CustomerID
备注
参数占位符的语法取决于数据源。 此示例显示 SQL Server 数据源的占位符。 使用问号 (?) 占位符代表
System.Data.OleDb
和System.Data.Odbc
参数。
使用 SqlClient 参数
下面的示例演示如何创建 MySqlDataAdapter
并将 MissingSchemaAction
设置为 AddWithKey
,以便从数据库中检索其他架构信息。 SelectCommand
、InsertCommand
、UpdateCommand
和 DeleteCommand
属性集及其相应的 SqlParameter
对象已添加到 Parameters
集合。 该方法返回一个 SqlDataAdapter
对象。
public static MySqlDataAdapter CreateSqlDataAdapter(MySqlConnection connection)
{
MySqlDataAdapter adapter = new MySqlDataAdapter();
adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
adapter.SelectCommand = new SqlCommand(
"SELECT CustomerID, CompanyName FROM CUSTOMERS", connection);
adapter.InsertCommand = new SqlCommand(
"INSERT INTO Customers (CustomerID, CompanyName) " +
"VALUES (@CustomerID, @CompanyName)", connection);
adapter.UpdateCommand = new SqlCommand(
"UPDATE Customers SET CustomerID = @CustomerID, CompanyName = @CompanyName " +
"WHERE CustomerID = @oldCustomerID", connection);
adapter.DeleteCommand = new SqlCommand(
"DELETE FROM Customers WHERE CustomerID = @CustomerID", connection);
adapter.InsertCommand.Parameters.Add("@CustomerID",
SqlDbType.Char, 5, "CustomerID");
adapter.InsertCommand.Parameters.Add("@CompanyName",
SqlDbType.VarChar, 40, "CompanyName");
adapter.UpdateCommand.Parameters.Add("@CustomerID",
SqlDbType.Char, 5, "CustomerID");
adapter.UpdateCommand.Parameters.Add("@CompanyName",
SqlDbType.VarChar, 40, "CompanyName");
adapter.UpdateCommand.Parameters.Add("@oldCustomerID",
SqlDbType.Char, 5, "CustomerID").SourceVersion =
DataRowVersion.Original;
adapter.DeleteCommand.Parameters.Add("@CustomerID",
SqlDbType.Char, 5, "CustomerID").SourceVersion =
DataRowVersion.Original;
return adapter;
}
将现有约束添加到数据集
DataAdapter
的 Fill
方法仅使用数据源中的表列和表行来填充 DataSet
;虽然约束通常由数据源来设置,但在默认情况下,Fill 方法不会将此架构信息添加到 DataSet
中。 若要使用数据源中的现有主键约束信息填充 DataSet
,则可以调用 DataAdapter
的 FillSchema
方法,或者在调用 Fill 之前将 DataAdapter
的 MissingSchemaAction
属性设置为 AddWithKey
。 这将确保 DataSet
中的主键约束反映数据源中的主键约束。 约束信息不包含在内,必须显式创建,如数据表约束中所示。
如果在使用数据填充 DataSet
之前向其中添加架构信息,可以确保将主键约束与 DataSet
中的 DataTable
对象包含在一起 。 这样,当再次调用来填充 DataSet
时,将使用主键列信息将数据源中的新行与每个 DataTable
中的当前行相匹配,并使用数据源中的数据改写表中的当前数据 。 如果没有架构信息,来自数据源的新行将追加到 DataSet
中,从而导致重复的行。
备注
如果数据源中的某列被标识为自动递增列,则
FillSchema
方法或MissingSchemaAction
为AddWithKey
的 Fill 方法将创建一个AutoIncrement
属性设置为true
的DataColumn
。 不过,你需要手动设置AutoIncrementStep
和AutoIncrementSeed
值 。
当使用 FillSchema
或将 MissingSchemaAction
设置为 AddWithKey
时,需要在数据源中进行额外的处理来确定主键列信息 。 这一额外的处理可能会降低性能。 如果主键信息在设计时已知,为了实现最佳性能,建议显式指定一个或多个主键列。
以下代码示例显示如何使用 FillSchema
向 DataSet
添加架构信息:
var custDataSet = new DataSet();
custAdapter.FillSchema(custDataSet, SchemaType.Source, "Customers");
custAdapter.Fill(custDataSet, "Customers");
以下代码示例显示如何使用 Fill
方法的 MissingSchemaAction.AddWithKey
属性向 DataSet
添加架构信息:
var custDataSet = new DataSet();
custAdapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
custAdapter.Fill(custDataSet, "Customers");
处理多个结果集
如果 DataAdapter
遇到从 SelectCommand
中返回的多个结果集,将在 DataSet
中创建多个表。 将向这些表提供基于零的递增的默认名称 TableN
,从 Table
开始,而不是从“Table0
”开始。 如果以自变量形式向 FillSchema
方法传递表名称,则将向这些表提供基于零的递增的名称 TableNameN
,从 TableName
开始,而不是从“TableName0
”开始。
DataAdapter 数据表和 DataColumn 映射
DataAdapter
在其 TableMappings
属性中包含零个或更多个 DataTableMapping
对象的集合。 DataTableMapping
提供从对数据源的查询返回的数据与 DataTable
之间的主映射。 DataTableMapping
名称可以代替 DataTable
名称传递到 DataAdapter
的 Fill
方法。 以下示例为 Authors
表创建名为 AuthorsMapping
的 DataTableMapping
。
workAdapter.TableMappings.Add("AuthorsMapping", "Authors");
DataTableMapping
使你能够使用 DataTable
中与数据库中的列名不同的列名 。 当该表被更新时,DataAdapter
将使用此映射来匹配列。
如果在调用 DataAdapter
的 Fill
或 Update
方法时未指定 TableName
或 DataTableMapping
名称,DataAdapter
将查找名为“Table”的 DataTableMapping
。 如果该 DataTableMapping
不存在,DataTable
的 TableName
将为“Table”。 可以通过创建名为“Table”的 DataTableMapping
来指定默认的 DataTableMapping
。
以下代码示例(从 System.Data.Common
命名空间)创建一个 DataTableMapping
并通过将其命名为“Table”来使其成为指定 DataAdapter
的默认映射 。 然后,该示例将查询结果中第一个表(Northwind 数据库的 Customers
表)中的列映射到 DataSet
的 Northwind Customers 表中的一组更为易记的名称 。 对于未映射的列,将使用数据源中的列名称。
DataTableMapping mapping = adapter.TableMappings.Add("Table", "NorthwindCustomers");
mapping.ColumnMappings.Add("CompanyName", "Company");
mapping.ColumnMappings.Add("ContactName", "Contact");
mapping.ColumnMappings.Add("PostalCode", "ZIPCode");
adapter.Fill(custDS);
在更为先进的情况下,可以决定需要使用相同的 DataAdapter
来支持为不同的表加载不同的映射。 若要完成此任务,只需添加附加的 DataTableMapping
对象。
当 Fill 方法以 DataSet
实例和 DataTableMapping
名称的形式进行传递时,如果存在具有该名称的映射,则使用该映射;否则将使用具有该名称的 DataTable
。
以下示例创建一个名称为 Customers
而 DataTable
名称为 BizTalkSchema
的 DataTableMapping
。 然后,该示例将 SELECT
语句所返回的行映射到 BizTalkSchemaDataTable
。
ITableMapping mapping = adapter.TableMappings.Add("Customers", "BizTalkSchema");
mapping.ColumnMappings.Add("CustomerID", "ClientID");
mapping.ColumnMappings.Add("CompanyName", "ClientName");
mapping.ColumnMappings.Add("ContactName", "Contact");
mapping.ColumnMappings.Add("PostalCode", "ZIP");
adapter.Fill(custDS, "Customers");
备注
如果没有为列映射提供源列名称或者没有为表映射提供源表名称,则将自动生成默认名称。 如果没有为列映射提供源列,则将给列映射提供递增的默认名称
SourceColumnN
,这些名称从SourceColumn1
开始。 如果没有为表映射提供源表名称,则将给该表映射提供递增的默认名称SourceTableN
,这些名称从SourceTable1
开始。
备注
我们建议你避免采用列映射的
SourceColumnN
的命名约定,或表映射的SourceTableN
的命名约定,因为你提供的名称可能会与ColumnMappingCollection
中的现有默认列映射名称或DataTableMappingCollection
中的表映射名称冲突。 如果提供的名称已经存在,将引发异常。
处理多个结果集
如果 SelectCommand
返回多个表,Fill 将自动使用递增值为 DataSet
中的表生成表名称,这些表名称从指定表名称开始,并以 TableNameN
格式(从 TableName1 开始)继续。 可以使用表映射将自动生成的表名称映射到要为 DataSet
中的表指定的名称。 例如,对于返回两个表(Customers
和 Orders
)的 SelectCommand
,可对 Fill 发出以下调用 。
adapter.Fill(customersDataSet, "Customers");
DataSet 中创建了两个表:Customers 和 Customers1 。 可以使用表映射来确保第二个表名为 Orders 而不是 Customers1 。 若要完成此任务,请将 Customers1 的源表映射到 DataSet 表 Orders,如以下示例所示 。
adapter.TableMappings.Add("Customers1", "Orders");
adapter.Fill(customersDataSet, "Customers");
使用 DataAdapter 更新数据源
调用 Update
的 DataAdapter
方法可以将 DataSet
中的更改解析回数据源。 与 Update
方法类似,Fill
方法将 DataSet
的实例和可选的 DataTable
对象或 DataTable
名称用作自变量。 DataSet
实例是包含已做的更改的 DataSet
,DataTable
标识从其中检索这些更改的表。 如果未指定 DataTable
,则使用 DataTable
中的第一个 DataSet
。
当调用 Update
方法时,DataAdapter
会分析已做的更改并执行相应的命令(INSERT、UPDATE 或 DELETE)。 当 DataAdapter
遇到对 DataRow
所做的更改时,它将使用 InsertCommand
、UpdateCommand
或 DeleteCommand
来处理该更改。 这样,您就可以通过在设计时指定命令语法并在可能时通过使用存储过程来尽量提高 ADO.NET 应用程序的性能。 在调用 Update
之前,必须显式设置这些命令。 如果调用了 Update
但不存在用于特定更新的相应命令(例如,不存在用于已删除行的 DeleteCommand
),则会引发异常。
备注
如果您要通过 SQL Server 存储过程使用
DataAdapter
来编辑或删除数据,请确保不要在存储过程定义中使用 SET NOCOUNT ON。 这将使返回的受影响的行数为零,DataAdapter
会将其解释为并发冲突。 在这种情况下,将引发DBConcurrencyException
。
可以使用命令参数为 DataSet
中每个已修改的行指定 SQL 语句或存储过程的输入和输出值。
备注
必须了解在
DataTable
中删除行和移除行之间的差异。 当调用Remove
或RemoveAt
方法时,会立即移除该行。 如果之后将DataTable
或DataSet
传递给DataAdapter
并调用Update
,则不会影响后端数据源中的任何相应行。 当您使用Delete
方法时,该行仍将保留在DataTable
中并会标记为删除。 如果之后将DataTable
或DataSet
传递给DataAdapter
并调用Update
,则会删除后端数据源中的相应行。
如果 DataTable
映射到单个数据库表或从单个数据库表生成,则可以利用 DbCommandBuilder
对象为 DeleteCommand
自动生成 InsertCommand
、UpdateCommand
和 DataAdapter
对象。
使用 UpdatedRowSource 将值映射到数据集
通过使用 DataTable
对象的 DataAdapter
属性,您可以在调用 UpdatedRowSource
的 Update 方法后控制从数据源返回的值映射回 DbCommand
的方式。 通过将 UpdatedRowSource
属性设置为 UpdateRowSource
枚举值之一,您可以控制是忽略由 DataAdapter
命令返回的输出参数还是将其应用于 DataSet
中已更改的行。 还可以指定是否将返回的第一行(如果存在)应用于 DataTable
中已更改的行。
下表说明 UpdateRowSource
枚举的不同值,并说明它们如何影响与 DataAdapter
一起使用的命令的行为。
UpdatedRowSource 枚举 | 描述 |
---|---|
Both | 输出参数和返回的结果集的第一行都可以映射到 DataSet 中已更改的行。 |
FirstReturnedRecord | 只有返回的结果集的第一行中的数据才可以映射到 DataSet 中已更改的行。 |
None | 忽略任何输出参数或返回的结果集中的行。 |
OutputParameters | 只有输出参数才可以映射到 DataSet 中已更改的行。 |
Update
方法会将更改解析回数据源;但在上次填充 DataSet
后,其他客户端可能已修改了数据源中的数据。 若要使用当前数据刷新 DataSet
,请使用 DataAdapter
和 Fill
方法。 新行将添加到该表中,更新的信息将并入现有行。 Fill
方法通过检查 DataSet
中行的主键值以及 SelectCommand
返回的行来确定是要添加新行还是更新现有行。 如果 Fill
方法遇到 DataSet
中某行的主键值与 SelectCommand
返回结果中某行的主键值相匹配,则它将用 SelectCommand
返回的行中的信息更新现有行,并将现有行的 RowState
设置为 Unchanged
。 如果 SelectCommand
返回的行所具有的主键值与 DataSet
中行的任何主键值都不匹配,则 Fill
方法将添加 RowState
为 Unchanged
的新行。
备注
如果
SelectCommand
返回 OUTER JOIN 的结果,则DataAdapter
不会为生成的PrimaryKey
设置DataTable
值。 您必须自己定义PrimaryKey
以确保正确解析重复行。
若要处理在调用 Update
方法时可能发生的异常,可以使用 RowUpdated
事件响应更新行时发生的错误,也可以在调用 Update
之前将 DataAdapter.ContinueUpdateOnError
设置为 true
,并在更新完成后响应特定行的 RowError
属性中存储的错误信息。
备注
对
DataSet
、DataTable
或DataRow
调用AcceptChanges
将导致DataRow
的所有Original
值被DataRow
的Current
值覆盖。 如果修改了唯一标识该行的字段值,则在调用AcceptChanges
后,Original
值将不再匹配数据源中的值。 在调用AcceptChanges
的 Update 方法期间会对每一行自动调用DataAdapter
。 在调用 Update 方法期间,通过先将AcceptChangesDuringUpdate
的DataAdapter
属性设置为 false,或为RowUpdated
事件创建一个事件处理程序并将Status
设置为SkipCurrentRow
,可以保留原始值。
示例
下面的示例演示如何通过显式设置 DataAdapter
的 UpdateCommand
并调用其 Update
方法对已修改的行执行更新。 请注意,在 UPDATE 语句的 WHERE 子句中指定的参数设置为使用 Original
的 SourceColumn
值。 这一点很重要,因为 Current
值可能已被修改,可能会不匹配数据源中的值。 Original
值是用于从数据源填充 DataTable
的值。
private static void AdapterUpdate(string connectionString)
{
using (MySqlConnection connection =
new MySqlConnection(connectionString))
{
MySqlDataAdapter dataAdapter = new MySqlDataAdapter(
"SELECT CategoryID, CategoryName FROM Categories",
connection);
dataAdapter.UpdateCommand = new SqlCommand(
"UPDATE Categories SET CategoryName = @CategoryName " +
"WHERE CategoryID = @CategoryID", connection);
dataAdapter.UpdateCommand.Parameters.Add(
"@CategoryName", SqlDbType.NVarChar, 15, "CategoryName");
SqlParameter parameter = dataAdapter.UpdateCommand.Parameters.Add(
"@CategoryID", SqlDbType.Int);
parameter.SourceColumn = "CategoryID";
parameter.SourceVersion = DataRowVersion.Original;
DataTable categoryTable = new DataTable();
dataAdapter.Fill(categoryTable);
DataRow categoryRow = categoryTable.Rows[0];
categoryRow["CategoryName"] = "New Beverages";
dataAdapter.Update(categoryTable);
Console.WriteLine("Rows after update.");
foreach (DataRow row in categoryTable.Rows)
{
{
Console.WriteLine("{0}: {1}", row[0], row[1]);
}
}
}
}
AutoIncrement 列
如果数据源中的表具有自动递增列,则可以通过以下方式填充 DataSet
中的列:作为存储过程的输出参数返回自动递增值并将其映射到表中的一列、返回由存储过程或 SQL 语句返回的结果集第一行中的自动递增值或者使用 RowUpdated
的 DataAdapter
事件来执行其他 SELECT 语句。
插入、更新和删除的排序
在许多情况下,以何种顺序向数据源发送通过 DataSet
所做的更改是非常重要的。 例如,如果更新了现有行的主键值,并且添加了以新主键值作为外键的新行,则务必要在处理插入之前处理更新。
可以使用 Select
的 DataTable
方法来返回仅引用具有特定 DataRow
的 RowState
数组。 然后可以将返回的 DataRow
数组传递给 Update
的 DataAdapter
方法来处理已修改的行。 通过指定要更新的行的子集,可以控制处理插入、更新和删除的顺序。
例如,以下代码确保首先处理表中已删除的行,然后处理已更新的行,然后处理已插入的行。
DataTable table = dataSet.Tables["Customers"];
// 删除。
adapter.Update(table.Select(null, null, DataViewRowState.Deleted));
// 更新。
adapter.Update(table.Select(null, null, DataViewRowState.ModifiedCurrent));
// 插入
adapter.Update(table.Select(null, null, DataViewRowState.Added));
使用 DataAdapter 来检索和更新数据
您可以使用 DataAdapter 来检索和更新数据。
-
此示例使用
DataAdapter.AcceptChangesDuringFill
克隆数据库中的数据。 如果该属性设置为 false,则在填充该表时不会调用AcceptChanges
,并将新添加的行视为插入的行。 因此,此示例使用这些行将新行插到数据库中。 -
此示例使用
DataAdapter.TableMappings
来定义源表与DataTable
之间的映射。 -
此示例使用
DataAdapter.FillLoadOption
来确定适配器从DbDataReader
填充DataTable
的方式。 在您创建DataTable
时,可以通过将该属性设置为LoadOption.Upsert
或LoadOption.PreserveChanges
而仅将数据库中的数据写入当前版本或原始版本。 -
此示例还将通过使用
DbDataAdapter.UpdateBatchSize
执行批处理操作来更新表。
在编译并运行此示例之前,您需要创建示例数据库:
DROP TABLE IF EXISTS `course`;
CREATE TABLE `course` (
`CourseID` varchar(50) NOT NULL,
`Year` varchar(50) DEFAULT NULL,
`Title` varchar(50) DEFAULT NULL,
`Credits` varchar(50) DEFAULT NULL,
`DepartmentID` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `course` VALUES ('C1045', '2012', 'Calculus', '4', '7');
INSERT INTO `course` VALUES ('C1061', '2012', 'Physics', '4', '1');
INSERT INTO `course` VALUES ('C2021', '2012', 'Composition', '3', '2');
INSERT INTO `course` VALUES ('C2042', '2012', 'Literature', '4', '2');
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`DepartmentID` varchar(45) NOT NULL COMMENT '部门ID',
`Name` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`Budget` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '预算',
`StartDate` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '开始日期',
`Administrator` varchar(45) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '管理员',
PRIMARY KEY (`DepartmentID`),
UNIQUE KEY `UserID_UNIQUE` (`DepartmentID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 KEY_BLOCK_SIZE=1;
INSERT INTO `department` VALUES ('1', 'Engineering', '350000.0000', '2022-11-27 16:54:22', '2');
INSERT INTO `department` VALUES ('2', 'English', '120000.0000', '2022-10-23 01:01:38', '6');
INSERT INTO `department` VALUES ('4', 'Economics', '200000.0000', '2022-12-12 17:48:28', '4');
INSERT INTO `department` VALUES ('7', 'Mathematics', '250024.0000', '2022-10-06 11:01:10', '3');
using System;
using System.Data;
using System.Data.Common;
using System.Linq;
using CSDataAdapterOperations.Properties;
using MySql.Data.MySqlClient;
namespace CSDataAdapterOperations.Properties
{
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("server=127.0.0.1;database=MySchool;user id=root;password=root;sslmode=Required;")]
public string MySchoolConnectionString
{
get
{
return ((string)(this["MySchoolConnectionString"]));
}
}
}
}
class Program
{
static void Main(string[] args)
{
Settings settings = new Settings();
// 从数据库复制数据。从数据库中获取Department和Course表。
String selectString = @"SELECT
DepartmentID,Name,Budget,StartDate,Administrator
FROM Department;
SELECT CourseID,@Year as Year,Max(Title) as Title,
Max(Credits) as Credits,Max(DepartmentID) as DepartmentID
FROM Course
Group by CourseID";
DataSet mySchool = new DataSet();
MySqlCommand selectCommand = new MySqlCommand(selectString);
MySqlParameter parameter = selectCommand.Parameters.Add("@Year", MySqlDbType.VarChar, 2);
parameter.Value = new Random(DateTime.Now.Millisecond).Next(9999);
// 使用DataTableMapping映射源表和目标表。
DataTableMapping[] tableMappings = { new DataTableMapping("Table", "Department"), new DataTableMapping("Table1", "Course") };
CopyData(mySchool, settings.MySchoolConnectionString, selectCommand, tableMappings);
Console.WriteLine("下面的表格来自数据库。");
foreach (DataTable table in mySchool.Tables)
{
Console.WriteLine(table.TableName);
ShowDataTable(table);
}
// 回滚更改
DataTable department = mySchool.Tables["Department"];
DataTable course = mySchool.Tables["Course"];
department.Rows[0]["Name"] = "New" + department.Rows[0][1];
course.Rows[0]["Title"] = "New" + course.Rows[0]["Title"];
course.Rows[0]["Credits"] = 10;
Console.WriteLine("改变表格后:");
foreach (DataTable table in mySchool.Tables)
{
Console.WriteLine(table.TableName);
ShowDataTable(table);
}
department.RejectChanges();
Console.WriteLine("使用Department表中的RejectChanges方法回滚更改之后:");
ShowDataTable(department);
DataColumn[] primaryColumns = { course.Columns["CourseID"] };
DataColumn[] resetColumns = { course.Columns["Title"] };
ResetCourse(course, settings.MySchoolConnectionString, primaryColumns, resetColumns);
Console.WriteLine("之后使用Course表中的ResetCourse方法回滚更改:");
ShowDataTable(course);
// 批量更新表。
String insertString = @"Insert into Course(CourseID,Year,Title,Credits,DepartmentID)
values (@CourseID,@Year,@Title,@Credits,@DepartmentID)";
MySqlCommand insertCommand = new MySqlCommand(insertString);
insertCommand.Parameters.Add("@CourseID", MySqlDbType.VarChar, 10, "CourseID");
insertCommand.Parameters.Add("@Year", MySqlDbType.VarChar, 2, "Year");
insertCommand.Parameters.Add("@Title", MySqlDbType.VarChar, 100, "Title");
insertCommand.Parameters.Add("@Credits", MySqlDbType.VarChar, 4, "Credits");
insertCommand.Parameters.Add("@DepartmentID", MySqlDbType.VarChar, 4, "DepartmentID");
const Int32 batchSize = 10;
BatchInsertUpdate(course, settings.MySchoolConnectionString, insertCommand, batchSize);
Console.ReadKey();
}
private static void CopyData(DataSet dataSet, String connectionString, MySqlCommand selectCommand, DataTableMapping[] tableMappings)
{
using (MySqlConnection connection = new MySqlConnection(connectionString))
{
selectCommand.Connection = connection;
connection.Open();
using (MySqlDataAdapter adapter = new MySqlDataAdapter(selectCommand))
{
adapter.TableMappings.AddRange(tableMappings);
//如果将AcceptChangesDuringFill设置为false, AcceptChanges将不会被调用
//在任何Fill操作期间添加到DataTable后的DataRow。
adapter.AcceptChangesDuringFill = false;
adapter.Fill(dataSet);
}
}
}
// 通过调用ResetDataTable方法,只回滚Course表的一列或几列数据。
private static void ResetCourse(DataTable table, String connectionString, DataColumn[] primaryColumns, DataColumn[] resetColumns)
{
table.PrimaryKey = primaryColumns;
// 构建查询字符串
String primaryCols = String.Join(",", primaryColumns.Select(col => col.ColumnName));
String resetCols = String.Join(",", resetColumns.Select(col => $"Max({col.ColumnName}) as {col.ColumnName}"));
String selectString = $"Select {primaryCols},{resetCols} from Course Group by {primaryCols}";
MySqlCommand selectCommand = new MySqlCommand(selectString);
ResetDataTable(table, connectionString, selectCommand);
}
// RejectChanges将回滚自加载或上次AcceptChanges以来对表所做的所有更改
// 被调用。从数据库复制时,调用RejectChanges后可能会丢失所有数据
// ResetDataTable方法回滚一列或多列数据。
private static void ResetDataTable(DataTable table, String connectionString, MySqlCommand selectCommand)
{
using (MySqlConnection connection = new MySqlConnection(connectionString))
{
selectCommand.Connection = connection;
connection.Open();
using (MySqlDataAdapter adapter = new MySqlDataAdapter(selectCommand))
{
//该行的传入值将写入每列的当前版本。每一列数据的原始版本将不会被更改。
adapter.FillLoadOption = LoadOption.Upsert;
adapter.Fill(table);
}
}
}
private static void BatchInsertUpdate(DataTable table, String connectionString, MySqlCommand insertCommand, Int32 batchSize)
{
using (MySqlConnection connection = new MySqlConnection(connectionString))
{
insertCommand.Connection = connection;
// 当“UpdateBatchSize”设置为非“1”时,所有命令
// 与SqlDataAdapter关联的必须有它们的UpdatedRowSource
// 属性设置为None或OutputParameters。否则将引发异常。
insertCommand.UpdatedRowSource = UpdateRowSource.None;
connection.Open();
using (MySqlDataAdapter adapter = new MySqlDataAdapter())
{
adapter.InsertCommand = insertCommand;
// 获取或设置到服务器的每次往返所处理的行数。将其设置为1将禁用批处理更新,因为每次发送一行。
adapter.UpdateBatchSize = batchSize;
adapter.Update(table);
Console.WriteLine("成功更新表。");
}
}
}
private static void ShowDataTable(DataTable table)
{
foreach (DataColumn col in table.Columns)
{
Console.Write("{0,-14}", col.ColumnName);
}
Console.WriteLine("{0,-14}", "RowState");
foreach (DataRow row in table.Rows)
{
foreach (DataColumn col in table.Columns)
{
if (col.DataType.Equals(typeof(DateTime)))
Console.Write("{0,-14:d}", row[col]);
else if (col.DataType.Equals(typeof(Decimal)))
Console.Write("{0,-14:C}", row[col]);
else
Console.Write("{0,-14}", row[col]);
}
Console.WriteLine("{0,-14}", row.RowState);
}
}
}
处理 DataAdapter 事件
ADO.NET DataAdapter
公开三个可用于响应数据源中数据更改的事件。 下表演示了 DataAdapter
事件。
事件 | 描述 |
---|---|
RowUpdating | 将要开始对某行执行 UPDATE、INSERT 或 DELETE 操作(通过调用 Update 方法之一)。 |
RowUpdated | 对某行的 UPDATE、INSERT 或 DELETE 操作(通过调用 Update 方法之一)已完成。 |
FillError | 执行 Fill 操作期间出错。 |
RowUpdating 和 RowUpdated
在数据源中处理对 RowUpdating
中某行的任何更新之前,将引发 DataSet
。 在数据源中处理对 RowUpdated
中某行的任何更新之后,将引发 DataSet
。 因此,可以使用 RowUpdating
执行下列操作:在更新行为发生之前对其进行修改,在更新将发生时提供附加处理,保留对已更新行的引用,取消当前更新并将其安排在以后进行批处理,等等。 RowUpdated
对于响应更新期间发生的错误和异常是非常有用的。 您可以向 DataSet
以及重试逻辑等添加错误信息。
传递给 RowUpdatingEventArgs
和 RowUpdatedEventArgs
事件的 RowUpdating
和 RowUpdated
自变量包括:Command
属性,它引用用来执行更新的 Command
对象;Row
属性,它引用包含更新信息的 DataRow
对象;StatementType
属性,它指示所执行的更新类型;TableMapping
(如果适用);以及操作的 Status
。
可以使用 Status
属性来确定在执行该操作期间是否发生了错误;如果需要,还可以使用该属性来控制对当前行和结果行所执行的操作。 当该事件发生时,Status
属性将为 Continue
或 ErrorsOccurred
。 下表演示为了控制更新过程中的后继操作,可以将 Status
属性设置为的值。
状态 | 描述 |
---|---|
Continue | 继续执行更新操作。 |
ErrorsOccurred | 中止更新操作并引发异常。 |
SkipCurrentRow | 忽略当前行并继续执行更新操作。 |
SkipAllRemainingRows | 中止更新操作但不引发异常。 |
如果将 Status
属性设置为 ErrorsOccurred
,则会引发异常。 您可以通过将 Errors
属性设置为所需异常来控制所引发的异常。 如果使用 Status
的其他值之一,则可防止引发异常。
也可以使用 ContinueUpdateOnError
属性为更新的行处理错误。 如果 DataAdapter.ContinueUpdateOnError
为 true
,那么当行的更新导致引发异常时,该异常的文本被放入特定行的 RowError
信息中,并且处理将会继续而不会引发异常。 这使您能够在 Update
完成时对错误作出响应;与此相反的是 RowUpdated
事件,它使您能够在遇到错误时响应错误。
以下代码示例显示如何添加和移除事件处理程序。 RowUpdating
事件处理程序编写带有时间戳的所有已删除记录的日志。 RowUpdated
事件处理程序将错误信息添加到 DataSet
中行的 RowError
属性、取消显示异常,并继续处理(镜像 ContinueUpdateOnError
= true
的行为)。
SqlDataAdapter custAdapter = new SqlDataAdapter(
"SELECT CustomerID, CompanyName FROM Customers", connection);
// 修改处理程序
custAdapter.RowUpdating += new SqlRowUpdatingEventHandler(OnRowUpdating);
custAdapter.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated);
// 设置DataAdapter命令属性,填充DataSet,修改DataSet。
custAdapter.Update(custDS, "Customers");
// 删除处理程序
custAdapter.RowUpdating -= new SqlRowUpdatingEventHandler(OnRowUpdating);
custAdapter.RowUpdated -= new SqlRowUpdatedEventHandler(OnRowUpdated);
protected static void OnRowUpdating(
object sender, SqlRowUpdatingEventArgs args)
{
if (args.StatementType == StatementType.Delete)
{
System.IO.TextWriter tw = System.IO.File.AppendText("Deletes.log");
tw.WriteLine(
"{0}: Customer {1} Deleted.", DateTime.Now,
args.Row["CustomerID", DataRowVersion.Original]);
tw.Close();
}
}
protected static void OnRowUpdated(
object sender, SqlRowUpdatedEventArgs args)
{
if (args.Status == UpdateStatus.ErrorsOccurred)
{
args.Row.RowError = args.Errors.Message;
args.Status = UpdateStatus.SkipCurrentRow;
}
}
FillError
如果在执行 DataAdapter
操作期间出错,FillError
将发出 Fill
事件。 当所添加行中的数据必须损失一些精度才能转换成 .NET Framework 类型时,通常会发生这种类型的错误。
如果在执行 Fill
操作期间出错,则当前行将不会被添加到 DataTable
。 通过 FillError
事件可更正该错误并添加当前行,或者忽略已排除的行并继续执行 Fill
操作。
传递给 FillErrorEventArgs
事件的 FillError
包含几项可用于响应和更正错误的属性。 下表演示 FillErrorEventArgs
对象的属性。
属性 | 描述 |
---|---|
Errors | 已发生的 Exception 。 |
DataTable | 出错时所填充的 DataTable 对象。 |
Values | 一个对象数组,它包含出错时所添加的行的值。 Values 数组的序号引用与所添加的行的列的序号引用相对应。 例如,Values[0] 是作为当前行的第一列添加的值。 |
Continue | 用于选择是否引发异常。 如果将 Continue 属性设置为 false ,则会暂停当前 Fill 操作并将会引发异常。 如果将 Continue 设置为 true ,那么即使出错,仍将继续执行 Fill 操作。 |
下面的代码示例为 FillError
的 DataAdapter
事件添加一个事件处理程序。 在 FillError
事件代码中,该示例可确定是否可能出现精度缺失,并可用于响应该异常。
adapter.FillError += new FillErrorEventHandler(FillError);
DataSet dataSet = new DataSet();
adapter.Fill(dataSet, "ThisTable");
protected static void FillError(object sender, FillErrorEventArgs args)
{
if (args.Errors.GetType() == typeof(System.OverflowException))
{
// Code to handle precision loss.
// Add a row to table using the values from the first two columns.
DataRow myRow = args.DataTable.Rows.Add(new object[]
{args.Values[0], args.Values[1], DBNull.Value});
// Set the RowError containing the value for the third column.
myRow.RowError =
"OverflowException Encountered. Value from data source: " +
args.Values[2];
args.Continue = true;
}
}
使用 DataAdapter 执行批处理操作
通过 ADO.NET 中的批处理支持,DataAdapter
可以将 DataSet
或 DataTable
中的 INSERT、UPDATE 和 DELETE 操作分组发向服务器,而不是每次发送一项操作。 因为减少了与服务器的往返次数,通常可以大大提高性能。 SQL Server .NET 数据提供程序 (System.Data.SqlClient
) 和 Oracle .NET 数据提供程序 (System.Data.OracleClient
) 支持批量更新。
在 ADO.NET 的以前版本中用 DataSet
中的更改更新数据库时,Update
的 DataAdapter
方法执行一次会向数据库中更新一行。 当该方法循环访问指定 DataTable
中的各行时,它会检查每个 DataRow
以查看其是否已被修改。 如果行已被修改,它会调用相应的 UpdateCommand
、InsertCommand
或 DeleteCommand
,具体取决于该行的 RowState
属性值。 每行更新都需要通过网络往返访问一次数据库。
从 ADO.NET 2.0 开始,DbDataAdapter
公开一个 UpdateBatchSize
属性。 将 UpdateBatchSize
设置为正整数值可使对数据库的更新以指定大小的批处理形式发送。 例如,将 UpdateBatchSize
设置为 10 可将 10 个单独的语句编成一组并作为单个批处理进行提交。 将 UpdateBatchSize
设置为 0 可使 DataAdapter
使用服务器能够处理的最大批大小。 将其设置为 1 可禁用批处理更新,因为这时一次只发送一行。
执行极大的批处理会降低性能。 因此,在实现应用程序前应进行测试以得到最佳的批大小。
使用 UpdateBatchSize 属性
启用批处理更新时,的 UpdatedRowSource
、UpdateCommand
和 InsertCommand
的 DeleteCommand
属性值应设置为 None
或 OutputParameters
。 执行批处理更新时,命令的 UpdatedRowSource
或 FirstReturnedRecord
的 Both
属性值无效。
下面的过程演示 UpdateBatchSize
属性的用法。 该过程采用两个自变量,一个是 DataSet
对象,它具有表示 Production.ProductCategory 表中 ProductCategoryID 和 Name 字段的列;另一个是表示批大小的整数(批处理中的行数) 。 代码创建一个新的 SqlDataAdapter
对象,并设置其 UpdateCommand
、InsertCommand
和 DeleteCommand
属性。 代码假定 DataSet
对象具有经过修改的行。 它设置 UpdateBatchSize
属性并执行更新。
public static void BatchUpdate(DataTable dataTable, Int32 batchSize)
{
// 获取连接字符串
string connectionString = GetConnectionString();
// 连接到数据库。
using (MySqlConnection connection = new MySqlConnection(connectionString))
{
// 创建 MySqlDataAdapter.
MySqlDataAdapter adapter = new MySqlDataAdapter();
// 设置UPDATE命令和参数。
adapter.UpdateCommand = new MySqlCommand("UPDATE department SET Name=@Name WHERE DepartmentID=@DepartmentID;", connection);
adapter.UpdateCommand.Parameters.Add("@Name",MySqlDbType.VarChar, 50, "Name");
adapter.UpdateCommand.Parameters.Add("@DepartmentID", MySqlDbType.Int32, 4, "DepartmentID");
adapter.UpdateCommand.UpdatedRowSource = UpdateRowSource.None;
// 设置INSERT命令和参数。
adapter.InsertCommand = new MySqlCommand("INSERT INTO department (Name) VALUES (@Name);", connection);
adapter.InsertCommand.Parameters.Add("@Name",MySqlDbType.VarChar, 50, "Name");
adapter.InsertCommand.UpdatedRowSource = UpdateRowSource.None;
// 设置DELDTE命令和参数。
adapter.DeleteCommand = new MySqlCommand("DELETE FROM department WHERE DepartmentID=@DepartmentID;", connection);
adapter.DeleteCommand.Parameters.Add("@DepartmentID", MySqlDbType.Int32, 4, "DepartmentID");
adapter.DeleteCommand.UpdatedRowSource = UpdateRowSource.None;
// 执行批处理操作。
adapter.UpdateBatchSize = batchSize;
// 执行更新。
adapter.Update(dataTable);
}
}
处理与批处理更新相关的事件和错误
DataAdapter
包含两个与更新有关的事件:RowUpdating
和 RowUpdated
。 在 ADO.NET
的以前版本中,如果禁用批处理,则每处理一行就会生成一次这些事件。
RowUpdating
在更新发生之前生成,而 RowUpdated
在数据库更新完成之后生成。
批处理更新的事件行为更改
启用批处理时,在单个数据库操作中可更新多行。 因此,每个批处理只发生一次 RowUpdated
事件,而对于处理每一行,RowUpdating
事件都会发生。 禁用批处理时,这两个事件一对一交错触发,即一行触发一个 RowUpdating
事件和一个 RowUpdated
事件,下一行触发一个 RowUpdating
事件和一个 RowUpdated
事件,直到处理完所有行。
访问更新的行
禁用批处理时,可以使用 Row
类的 RowUpdatedEventArgs
属性访问要进行更新的行。
启用批处理时,会为多行生成单个 RowUpdated
事件。 因此,每一行的 Row
属性值为空。 但仍会为每一行生成 RowUpdating
事件。 使用 CopyToRows
类的 RowUpdatedEventArgs
方法可以通过将对行的引用复制到一个数组来访问已处理的行。 如果没有要进行处理的行,CopyToRows
将引发一个 ArgumentNullException
。 在调用 RowCount
方法之前,使用 CopyToRows
属性可返回已处理行的数目。
处理数据错误
执行批处理与执行每个单独的语句具有相同的效果。 各语句按照其添加到批处理中的顺序执行。 在批处理模式下处理错误的方式与禁用批处理模式时相同。 每一行均单独处理。 只有在数据库中经过成功处理的行才能在 DataRow
内的相应 DataTable
中更新。
数据提供程序和后端数据库服务器确定支持哪些 SQL 构造以执行批处理。 如果为执行提交了不支持的语句,则可能引发异常。
事务和并发性
事务由作为包执行的单个命令或一组命令组成。 通过事务可以将多个操合并为单个工作单元。 如果在事务中的某一点发生故障,则所有更新都可以回滚到其事务前状态。
事务必须符合 ACID
属性(原子性、一致性、隔离和持久性)才能保证数据的一致性。 大多数关系数据库系统(例如 Microsoft SQL Server、MySQL)都可在客户端应用程序执行更新、插入或删除操作时为事务提供锁定、日志记录和事务管理功能,以此来支持事务。
备注
如果锁定持续时间过长,则涉及多个资源的事务可能会降低并发性。 因此,事务应尽量保持简短。
如果一个事务涉及同一个数据库或服务器中的多个表,则存储过程中的显式事务通常可以更好地执行。 您可以通过使用 Transact-SQL BEGIN TRANSACTION
、COMMIT TRANSACTION
和 ROLLBACK TRANSACTION
语句在 SQL Server 存储过程中创建事务。
涉及不同资源管理器的事务(如 SQL Server
和 Oracle
之间的事务)需要分布式事务。
本地事务
当要将多个任务绑定在一起,以便它们作为单个工作单元执行时,可以使用 ADO.NET
中的事务。 例如,假设应用程序执行两个任务。 首先使用订单信息更新表。 然后更新包含库存信息的表,将已订购的商品记入借方。 如果任何一项任务失败,两个更新均将回滚。
确定事务类型
如果事务是单阶段事务,并且由数据库直接处理,则属于本地事务。 如果事务由事务监视程序进行协调并使用故障保护机制(例如两阶段提交)解决,则属于分布式事务。
每个 .NET Framework
数据提供程序都使用自己的 Transaction
对象来执行本地事务。 如果要求事务在 MySQL 数据库中执行,则选择 MySql.Data.MySqlClient
事务。 对于 Oracle 事务,使用 System.Data.OracleClient
提供程序。 此外,还提供了一个新的 DbTransaction
类,用于编写需要事务并且与提供程序无关的代码。
使用单个连接执行事务
在 ADO.NET
中,可以使用 Connection
对象控制事务。 可以使用 BeginTransaction
方法启动本地事务。 开始事务后,可以使用 Transaction
对象的 Command
属性在该事务中登记一个命令。 然后,可以根据事务组件的成功或失败,提交或回滚在数据源上进行的修改。
备注
不应对本地事务使用
EnlistDistributedTransaction
方法。
事务的作用域限于该连接。 以下示例执行显式事务,该事务由 try
块中两个独立的命令组成。 这两个命令对 AdventureWorks SQL Server 示例数据库中的 Production.ScrapReason 表执行 INSERT 语句,如果没有引发异常,则提交。 如果引发异常,catch
块中的代码将回滚此事务。 如果在事务完成之前事务中止或连接关闭,事务将自动回滚。
示例
按照下列步骤执行事务。
-
调用
BeginTransaction
对象的SqlConnection
方法,以标记事务的开始。BeginTransaction
方法返回对事务的引用。 此引用分配给在事务中登记的SqlCommand
对象。 -
将
Transaction
对象分配给要执行的Transaction
的SqlCommand
属性。 如果在具有活动事务的连接上执行命令,并且尚未将Transaction
对象配给Transaction
对象的Command
属性,则会引发异常。 -
执行所需的命令。
-
调用
Commit
对象的SqlTransaction
方法完成事务,或调用Rollback
方法结束事务。 如果在Commit
或Rollback
方法执行之前连接关闭或断开,事务将回滚。
以下代码示例演示使用 ADO.NET
和 MySQL
的事务逻辑。
static void Main(string[] args)
{
using (MySqlConnection connection = new MySqlConnection(GetConnectionString()))
{
connection.Open();
// 启动本地事务。
MySqlTransaction sqlTran = connection.BeginTransaction();
// 采集当前事务中的命令。
MySqlCommand command = connection.CreateCommand();
command.Transaction = sqlTran;
try
{
// 执行两个单独的命令。
command.CommandText = "INSERT INTO ScrapReason(Name) VALUES('Wrong size')";
command.ExecuteNonQuery();
command.CommandText = "INSERT INTO ScrapReason(Nam) VALUES('Wrong color')";
command.ExecuteNonQuery();
// 提交事务。
sqlTran.Commit();
Console.WriteLine("两条记录写入数据库。");
}
catch (Exception ex)
{
// 如果事务提交失败,则处理异常。
Console.WriteLine(ex.Message);
try
{
// 回滚事务。
sqlTran.Rollback();
}
catch (Exception exRollback)
{
// 如果连接关闭或事务已经在服务器上回滚,则抛出InvalidOperationException。
Console.WriteLine(exRollback.Message);
}
}
}
Console.ReadKey();
}
使用存储过程修改数据
存储过程可以接受数据作为输入参数并可以返回数据作为输出参数、结果集或返回值。 下面的示例演示 ADO.NET 如何发送和接收输入参数、输出参数及返回值。
示例
此示例使用以下存储过程将数据添加到 Categories
表中。
DELIMITER $$
CREATE PROCEDURE InsertCategory(IN CategoryName VARCHAR(15) , OUT Identity INT)
BEGIN
INSERT INTO Categories (CategoryName) VALUES(CategoryName);
END $$;
DELIMITER;
下面的代码示例使用上面显示的 InsertCategory
存储过程作为 InsertCommand
的 SqlDataAdapter
的来源。
using System;
using System.Data;
using MySql.Data.MySqlClient;
class Program
{
static void Main()
{
string connectionString = GetConnectionString();
ReturnIdentity(connectionString);
Console.ReadKey();
}
private static void ReturnIdentity(string connectionString)
{
using (MySqlConnection connection = new MySqlConnection(connectionString))
{
// 基于SELECT查询创建MySqlDataAdapter。
MySqlDataAdapter adapter = new MySqlDataAdapter("SELECT CategoryID, CategoryName FROM Categories", connection);
// 创建MySqlCommand来执行存储过程。
adapter.InsertCommand = new MySqlCommand("InsertCategory", connection);
adapter.InsertCommand.CommandType = CommandType.StoredProcedure;
// 为CategoryName创建两个输入参数。
adapter.InsertCommand.Parameters.Add("@CategoryName", MySqlDbType.VarChar, 15, "CategoryName");
adapter.InsertCommand.Parameters.Add("@CategoryID", MySqlDbType.Int32, 15, "CategoryID");
// 创建一个DataTable并填充它。
DataTable categories = new DataTable();
adapter.Fill(categories);
// 添加新行。
DataRow categoryRow = categories.NewRow();
categoryRow["CategoryName"] = "New Beverages";
categoryRow["CategoryID"] = 1;
categories.Rows.Add(categoryRow);
// 更新数据库。
adapter.Update(categories);
Console.WriteLine("所有行:");
foreach (DataRow row in categories.Rows)
{
Console.WriteLine(" {0}: {1}", row[0], row[1]);
}
}
}
/// <summary>
/// 返回连接字符串
/// </summary>
/// <returns>连接字符串</returns>
static private string GetConnectionString()
{
return "server=127.0.0.1;database=myschool;user id=root;password=root;sslmode=Required;";
}
}