ADO.NET(MySQL)

目录

ADO.NET

.NET Framework 数据提供程序

ADO.NET 中的并行执行

在 ADO.NET 中检索和修改数据

DataAdapter 和 DataReader

事务和并发性

使用存储过程修改数据


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 TABLEINSERT INTO SQL 语句即可。 代码使用 MySqlConnection 类的以下方法:

  • OpenAsync(),用于与 MySQL 建立连接。

  • CreateCommand(),用于设置 CommandText 属性

  • ExecuteNonQueryAsync(),用于运行数据库命令。

ServerDatabaseUserIDPassword 参数替换为你在创建服务器和数据库时指定的值。

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.MySqlClientMySql.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)。

示例

此示例演示如何从配置文件中检索部分连接字符串并通过设置 DataSourceUserIDPasswordSqlConnectionStringBuilder 属性完成该连接字符串。 配置文件定义如下。

    <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 从数据库中检索只读、只进的数据流。 查询结果在查询执行时返回,在并存储在客户端的网络缓冲区中,直到使用 DataReaderRead 方法对它们发出请求 。 使用 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来控制。

SelectCommandDataAdapter 属性是一个 Command 对象,用于从数据源中检索数据。

InsertCommandUpdateCommandDeleteCommandDataAdapter 属性是 Command 对象,用于按照对 DataSet中数据的修改来管理对数据源中数据的更新。

FillDataAdapter 方法用于使用 DataSetSelectCommand 结果填充 。 Fill 将要填充的 DataSetDataAdapterDataTable 对象(或要使用从 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 参数用于创建 SelectCommandSqlDataAdapter属性。

示例
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 完成时关闭连接。 当处理单一操作(如 FillUpdate)时,这可以简化您的代码。 但是,如果您在执行多项需要打开连接的操作,则可以通过以下方式提高应用程序的性能:显式调用 OpenConnection方法,对数据源执行操作,然后调用 CloseConnection方法。 应尝试使数据源的连接打开的时间尽可能短,以便释放资源供其他客户端应用程序使用。

多个结果集

如果 DataAdapter 遇到多个结果集,则将在 DataSet中创建多个表。 这些表的命名方式为默认名称 Table 加上N,N 从 0 开始递增,如以 Table0 为第一个表名,依次类推。 如果以参数形式向 Fill 方法传递表名,则这些表的命名方式为默认名称 TableName 加上N,N 从 0 开始递增,如以 TableName0 为第一个表名,依次类推。

从多个 DataAdapter 填充 DataSet

一个 DataSet 可以与任意数量的 DataAdapter 对象一起使用。 每个 DataAdapter 都可用于填充一个或多个 DataTable 对象并将更新解析回相关数据源。 DataRelationConstraint 对象可以在本地添加到 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 属性返回数据源中的数据;

  • InsertCommandUpdateCommandDeleteCommand 属性用于管理数据源中的更新。

调用 SelectCommandFill 方法之前必须设置 DataAdapter 属性。

在调用 InsertCommandUpdateCommand 方法之前必须设置 DeleteCommandUpdateDataAdapter 属性,具体取决于对 DataTable 中的数据做了哪些更改。

例如,如果已添加行,在调用 InsertCommand 之前必须设置 Update。 当 Update 正在处理已插入、已更新或已删除的行时,DataAdapter 将使用相应的 Command 属性来处理该操作。 有关已修改行的当前信息将通过 Command 集合传递到 Parameters 对象。

更新数据源中的行时,调用 UPDATE 语句,该语句使用唯一标识符来标识表中要更新的行。 该唯一标识符通常是主键字段的值。 UPDATE 语句使用的参数既包含唯一标识符又包含要更新的列和值,如下面的 Transact-SQL 语句所示。

UPDATE Customers SET CompanyName = @CompanyName
  WHERE CustomerID = @CustomerID  

备注

参数占位符的语法取决于数据源。 此示例显示 SQL Server 数据源的占位符。 使用问号 (?) 占位符代表 System.Data.OleDbSystem.Data.Odbc 参数。

使用 SqlClient 参数

下面的示例演示如何创建 MySqlDataAdapter 并将 MissingSchemaAction 设置为 AddWithKey,以便从数据库中检索其他架构信息。 SelectCommandInsertCommandUpdateCommandDeleteCommand 属性集及其相应的 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;
}

将现有约束添加到数据集

DataAdapterFill 方法仅使用数据源中的表列和表行来填充 DataSet;虽然约束通常由数据源来设置,但在默认情况下,Fill 方法不会将此架构信息添加到 DataSet 中。 若要使用数据源中的现有主键约束信息填充 DataSet,则可以调用 DataAdapterFillSchema 方法,或者在调用 Fill 之前将 DataAdapterMissingSchemaAction 属性设置为 AddWithKey。 这将确保 DataSet 中的主键约束反映数据源中的主键约束。 约束信息不包含在内,必须显式创建,如数据表约束中所示。

如果在使用数据填充 DataSet 之前向其中添加架构信息,可以确保将主键约束与 DataSet 中的 DataTable 对象包含在一起 。 这样,当再次调用来填充 DataSet 时,将使用主键列信息将数据源中的新行与每个 DataTable 中的当前行相匹配,并使用数据源中的数据改写表中的当前数据 。 如果没有架构信息,来自数据源的新行将追加到 DataSet 中,从而导致重复的行。

备注

如果数据源中的某列被标识为自动递增列,则 FillSchema 方法或 MissingSchemaActionAddWithKey 的 Fill 方法将创建一个 AutoIncrement 属性设置为 trueDataColumn。 不过,你需要手动设置 AutoIncrementStepAutoIncrementSeed 值 。

当使用 FillSchema 或将 MissingSchemaAction 设置为 AddWithKey 时,需要在数据源中进行额外的处理来确定主键列信息 。 这一额外的处理可能会降低性能。 如果主键信息在设计时已知,为了实现最佳性能,建议显式指定一个或多个主键列。

以下代码示例显示如何使用 FillSchemaDataSet 添加架构信息:

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 名称传递到 DataAdapterFill 方法。 以下示例为 Authors 表创建名为 AuthorsMappingDataTableMapping

workAdapter.TableMappings.Add("AuthorsMapping", "Authors");  

DataTableMapping 使你能够使用 DataTable 中与数据库中的列名不同的列名 。 当该表被更新时,DataAdapter 将使用此映射来匹配列。

如果在调用 DataAdapterFillUpdate 方法时未指定 TableNameDataTableMapping 名称,DataAdapter 将查找名为“Table”的 DataTableMapping。 如果该 DataTableMapping 不存在,DataTableTableName 将为“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

以下示例创建一个名称为 CustomersDataTable 名称为 BizTalkSchemaDataTableMapping 。 然后,该示例将 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 中的表指定的名称。 例如,对于返回两个表(CustomersOrders)的 SelectCommand,可对 Fill 发出以下调用 。

adapter.Fill(customersDataSet, "Customers");  

DataSet 中创建了两个表:Customers 和 Customers1 。 可以使用表映射来确保第二个表名为 Orders 而不是 Customers1 。 若要完成此任务,请将 Customers1 的源表映射到 DataSet 表 Orders,如以下示例所示 。

adapter.TableMappings.Add("Customers1", "Orders");  
adapter.Fill(customersDataSet, "Customers");  

使用 DataAdapter 更新数据源

调用 UpdateDataAdapter 方法可以将 DataSet 中的更改解析回数据源。 与 Update 方法类似,Fill 方法将 DataSet 的实例和可选的 DataTable 对象或 DataTable 名称用作自变量。 DataSet 实例是包含已做的更改的 DataSetDataTable 标识从其中检索这些更改的表。 如果未指定 DataTable,则使用 DataTable 中的第一个 DataSet

当调用 Update 方法时,DataAdapter 会分析已做的更改并执行相应的命令(INSERT、UPDATE 或 DELETE)。 当 DataAdapter 遇到对 DataRow 所做的更改时,它将使用 InsertCommandUpdateCommandDeleteCommand 来处理该更改。 这样,您就可以通过在设计时指定命令语法并在可能时通过使用存储过程来尽量提高 ADO.NET 应用程序的性能。 在调用 Update 之前,必须显式设置这些命令。 如果调用了 Update 但不存在用于特定更新的相应命令(例如,不存在用于已删除行的 DeleteCommand),则会引发异常。

备注

如果您要通过 SQL Server 存储过程使用 DataAdapter 来编辑或删除数据,请确保不要在存储过程定义中使用 SET NOCOUNT ON。 这将使返回的受影响的行数为零,DataAdapter 会将其解释为并发冲突。 在这种情况下,将引发 DBConcurrencyException

可以使用命令参数为 DataSet 中每个已修改的行指定 SQL 语句或存储过程的输入和输出值。

备注

必须了解在 DataTable 中删除行和移除行之间的差异。 当调用 RemoveRemoveAt 方法时,会立即移除该行。 如果之后将 DataTableDataSet 传递给 DataAdapter 并调用 Update,则不会影响后端数据源中的任何相应行。 当您使用 Delete 方法时,该行仍将保留在 DataTable 中并会标记为删除。 如果之后将 DataTableDataSet 传递给 DataAdapter 并调用 Update,则会删除后端数据源中的相应行。

如果 DataTable 映射到单个数据库表或从单个数据库表生成,则可以利用 DbCommandBuilder 对象为 DeleteCommand 自动生成 InsertCommandUpdateCommandDataAdapter 对象。

使用 UpdatedRowSource 将值映射到数据集

通过使用 DataTable 对象的 DataAdapter 属性,您可以在调用 UpdatedRowSource 的 Update 方法后控制从数据源返回的值映射回 DbCommand 的方式。 通过将 UpdatedRowSource 属性设置为 UpdateRowSource 枚举值之一,您可以控制是忽略由 DataAdapter 命令返回的输出参数还是将其应用于 DataSet 中已更改的行。 还可以指定是否将返回的第一行(如果存在)应用于 DataTable 中已更改的行。

下表说明 UpdateRowSource 枚举的不同值,并说明它们如何影响与 DataAdapter 一起使用的命令的行为。

UpdatedRowSource 枚举描述
Both输出参数和返回的结果集的第一行都可以映射到 DataSet 中已更改的行。
FirstReturnedRecord只有返回的结果集的第一行中的数据才可以映射到 DataSet 中已更改的行。
None忽略任何输出参数或返回的结果集中的行。
OutputParameters只有输出参数才可以映射到 DataSet 中已更改的行。

Update 方法会将更改解析回数据源;但在上次填充 DataSet 后,其他客户端可能已修改了数据源中的数据。 若要使用当前数据刷新 DataSet,请使用 DataAdapterFill 方法。 新行将添加到该表中,更新的信息将并入现有行。 Fill 方法通过检查 DataSet 中行的主键值以及 SelectCommand 返回的行来确定是要添加新行还是更新现有行。 如果 Fill 方法遇到 DataSet 中某行的主键值与 SelectCommand 返回结果中某行的主键值相匹配,则它将用 SelectCommand 返回的行中的信息更新现有行,并将现有行的 RowState 设置为 Unchanged。 如果 SelectCommand 返回的行所具有的主键值与 DataSet 中行的任何主键值都不匹配,则 Fill 方法将添加 RowStateUnchanged 的新行。

备注

如果 SelectCommand 返回 OUTER JOIN 的结果,则 DataAdapter 不会为生成的 PrimaryKey 设置 DataTable 值。 您必须自己定义 PrimaryKey 以确保正确解析重复行。

若要处理在调用 Update 方法时可能发生的异常,可以使用 RowUpdated 事件响应更新行时发生的错误,也可以在调用 Update 之前将 DataAdapter.ContinueUpdateOnError 设置为 true,并在更新完成后响应特定行的 RowError 属性中存储的错误信息。

备注

DataSetDataTableDataRow 调用 AcceptChanges 将导致 DataRow 的所有 Original 值被 DataRowCurrent 值覆盖。 如果修改了唯一标识该行的字段值,则在调用 AcceptChanges 后,Original 值将不再匹配数据源中的值。 在调用 AcceptChanges 的 Update 方法期间会对每一行自动调用 DataAdapter。 在调用 Update 方法期间,通过先将 AcceptChangesDuringUpdateDataAdapter 属性设置为 false,或为 RowUpdated 事件创建一个事件处理程序并将 Status 设置为 SkipCurrentRow,可以保留原始值。

示例

下面的示例演示如何通过显式设置 DataAdapterUpdateCommand 并调用其 Update 方法对已修改的行执行更新。 请注意,在 UPDATE 语句的 WHERE 子句中指定的参数设置为使用 OriginalSourceColumn 值。 这一点很重要,因为 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 语句返回的结果集第一行中的自动递增值或者使用 RowUpdatedDataAdapter 事件来执行其他 SELECT 语句。

插入、更新和删除的排序

在许多情况下,以何种顺序向数据源发送通过 DataSet 所做的更改是非常重要的。 例如,如果更新了现有行的主键值,并且添加了以新主键值作为外键的新行,则务必要在处理插入之前处理更新。

可以使用 SelectDataTable 方法来返回仅引用具有特定 DataRowRowState 数组。 然后可以将返回的 DataRow 数组传递给 UpdateDataAdapter 方法来处理已修改的行。 通过指定要更新的行的子集,可以控制处理插入、更新和删除的顺序。

例如,以下代码确保首先处理表中已删除的行,然后处理已更新的行,然后处理已插入的行。

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.UpsertLoadOption.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 以及重试逻辑等添加错误信息。

传递给 RowUpdatingEventArgsRowUpdatedEventArgs 事件的 RowUpdatingRowUpdated 自变量包括:Command 属性,它引用用来执行更新的 Command 对象;Row 属性,它引用包含更新信息的 DataRow 对象;StatementType 属性,它指示所执行的更新类型;TableMapping(如果适用);以及操作的 Status

可以使用 Status 属性来确定在执行该操作期间是否发生了错误;如果需要,还可以使用该属性来控制对当前行和结果行所执行的操作。 当该事件发生时,Status 属性将为 ContinueErrorsOccurred。 下表演示为了控制更新过程中的后继操作,可以将 Status 属性设置为的值。

状态描述
Continue继续执行更新操作。
ErrorsOccurred中止更新操作并引发异常。
SkipCurrentRow忽略当前行并继续执行更新操作。
SkipAllRemainingRows中止更新操作但不引发异常。

如果将 Status 属性设置为 ErrorsOccurred,则会引发异常。 您可以通过将 Errors 属性设置为所需异常来控制所引发的异常。 如果使用 Status 的其他值之一,则可防止引发异常。

也可以使用 ContinueUpdateOnError 属性为更新的行处理错误。 如果 DataAdapter.ContinueUpdateOnErrortrue,那么当行的更新导致引发异常时,该异常的文本被放入特定行的 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 操作。

下面的代码示例为 FillErrorDataAdapter 事件添加一个事件处理程序。 在 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 可以将 DataSetDataTable 中的 INSERT、UPDATE 和 DELETE 操作分组发向服务器,而不是每次发送一项操作。 因为减少了与服务器的往返次数,通常可以大大提高性能。 SQL Server .NET 数据提供程序 (System.Data.SqlClient) 和 Oracle .NET 数据提供程序 (System.Data.OracleClient) 支持批量更新。

在 ADO.NET 的以前版本中用 DataSet 中的更改更新数据库时,UpdateDataAdapter 方法执行一次会向数据库中更新一行。 当该方法循环访问指定 DataTable 中的各行时,它会检查每个 DataRow 以查看其是否已被修改。 如果行已被修改,它会调用相应的 UpdateCommandInsertCommandDeleteCommand,具体取决于该行的 RowState 属性值。 每行更新都需要通过网络往返访问一次数据库。

从 ADO.NET 2.0 开始,DbDataAdapter 公开一个 UpdateBatchSize 属性。 将 UpdateBatchSize 设置为正整数值可使对数据库的更新以指定大小的批处理形式发送。 例如,将 UpdateBatchSize 设置为 10 可将 10 个单独的语句编成一组并作为单个批处理进行提交。 将 UpdateBatchSize 设置为 0 可使 DataAdapter 使用服务器能够处理的最大批大小。 将其设置为 1 可禁用批处理更新,因为这时一次只发送一行。

执行极大的批处理会降低性能。 因此,在实现应用程序前应进行测试以得到最佳的批大小。

使用 UpdateBatchSize 属性

启用批处理更新时,的 UpdatedRowSourceUpdateCommandInsertCommandDeleteCommand 属性值应设置为 NoneOutputParameters。 执行批处理更新时,命令的 UpdatedRowSourceFirstReturnedRecordBoth 属性值无效。

下面的过程演示 UpdateBatchSize 属性的用法。 该过程采用两个自变量,一个是 DataSet 对象,它具有表示 Production.ProductCategory 表中 ProductCategoryID 和 Name 字段的列;另一个是表示批大小的整数(批处理中的行数) 。 代码创建一个新的 SqlDataAdapter 对象,并设置其 UpdateCommandInsertCommandDeleteCommand 属性。 代码假定 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 包含两个与更新有关的事件:RowUpdatingRowUpdated 。 在 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 TRANSACTIONCOMMIT TRANSACTIONROLLBACK TRANSACTION 语句在 SQL Server 存储过程中创建事务。

涉及不同资源管理器的事务(如 SQL ServerOracle 之间的事务)需要分布式事务。

本地事务

当要将多个任务绑定在一起,以便它们作为单个工作单元执行时,可以使用 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 块中的代码将回滚此事务。 如果在事务完成之前事务中止或连接关闭,事务将自动回滚。

示例

按照下列步骤执行事务。

  1. 调用 BeginTransaction 对象的 SqlConnection 方法,以标记事务的开始。 BeginTransaction 方法返回对事务的引用。 此引用分配给在事务中登记的 SqlCommand 对象。

  2. Transaction 对象分配给要执行的 TransactionSqlCommand 属性。 如果在具有活动事务的连接上执行命令,并且尚未将 Transaction 对象配给 Transaction 对象的 Command 属性,则会引发异常。

  3. 执行所需的命令。

  4. 调用 Commit 对象的 SqlTransaction 方法完成事务,或调用 Rollback 方法结束事务。 如果在 CommitRollback 方法执行之前连接关闭或断开,事务将回滚。

以下代码示例演示使用 ADO.NETMySQL 的事务逻辑。

        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 存储过程作为 InsertCommandSqlDataAdapter 的来源。

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;";
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值