将数据保存回 .NET Framework 应用程序中的数据库

注释

此类 DataSet 和相关类是 2000 年代初的旧版 .NET Framework 技术,使应用程序能够在应用与数据库断开连接时处理内存中的数据。 这些技术对于使用户能够修改数据并将更改保留回数据库的应用特别有用。 尽管数据集是经过证实的成功技术,但新 .NET 应用程序的建议方法是使用 Entity Framework Core。 Entity Framework 提供了一种更自然的方式来将表格数据用作对象模型,并且具有更简单的编程接口。

数据集是数据的内存中副本。 如果修改该数据,最好将这些更改保存回数据库。 您可以通过以下三种方式之一实现这一点:

  • 通过调用 TableAdapter 的 Update 方法之一

  • 通过调用 TableAdapter 的 DBDirect 方法之一

  • 通过对 TableAdapterManager 调用 UpdateAll 方法;当数据集包含与数据集中的其他表相关的表时,Visual Studio 将为你生成 TableAdapterManager

将数据集表绑定到 Windows 窗体或 XAML 页面上的控件时,数据绑定体系结构会为你完成所有工作。

如果你熟悉 TableAdapters,可以直接跳转到以下主题之一:

主题 DESCRIPTION
将新记录插入数据库中 如何使用 TableAdapters 或 Command 对象执行更新和插入
使用 TableAdapter 更新数据 如何使用 TableAdapters 执行更新
分层更新 如何从包含两个或多个相关表的数据集执行更新
处理并发异常 当两个用户尝试同时更改数据库中的相同数据时,如何处理异常
如何:使用事务保存数据 如何使用系统在事务中保存数据。 事务命名空间和 TransactionScope 对象
将数据保存在事务中 关于创建 Windows 窗体应用程序的演练,用于演示如何将数据保存到事务内的数据库中
将数据保存到数据库(多个表) 如何编辑记录并将多个表中的更改保存回数据库
将数据从对象保存到数据库 如何使用 TableAdapter DbDirect 方法将数据从不在数据集中的对象传递到数据库
使用 TableAdapter DBDirect 方法保存数据 如何使用 TableAdapter 将 SQL 查询直接发送到数据库
将数据集另存为 XML 如何将数据集保存到 XML 文档

两阶段式更新

更新数据源的过程分为两个步骤。 第一步是使用新记录、更改的记录或删除记录更新数据集。 如果应用程序永远不会将这些更改发送回数据源,则已完成更新。

如果确实将更改发送回数据库,则需要执行第二个步骤。 如果不使用数据绑定控件,则必须手动调用用于填充数据集的同一个 TableAdapter(或数据适配器)中的 Update 方法。 但是,还可以使用不同的适配器,例如,将数据从一个数据源移动到另一个数据源或更新多个数据源。 如果不使用数据绑定,并且要保存对相关表所做的更改,必须手动实例化自动生成的 TableAdapterManager 类的变量,然后调用其 UpdateAll 方法。

数据集更新的概念图

数据集包含多个表集合,每个表集合包含一组行。 如果稍后要更新基础数据源,则必须在添加或删除行时对 DataTable.DataRowCollection 属性使用方法。 这些方法执行更新数据源所需的更改跟踪。 如果在 Rows 属性上调用 RemoveAt 集合,则不会将删除内容传回数据库。

合并数据集

可以通过将其与其他数据集 合并 来更新数据集的内容。 这涉及到将 数据集的内容复制到调用数据集(称为 目标 数据集)。 合并数据集时,源数据集中的新记录将添加到目标数据集。 此外,源数据集中的额外列将添加到目标数据集。 当具有本地数据集并从另一个应用程序获取第二个数据集时,合并数据集非常有用。 从 XML Web 服务等组件获取第二个数据集或需要从多个数据集集成数据时,也很有用。

合并数据集时,可以传递一个布尔参数(preserveChanges),该参数指示 Merge 方法是否保留目标数据集中的现有修改。 由于数据集维护了多个版本的记录,因此请务必记住,正在合并多个版本的记录。 下表显示了如何合并两个数据集中的记录:

DataRowVersion 目标数据集 源数据集
原版 詹姆斯·威尔逊 詹姆斯·威尔逊
当前 吉姆·威尔逊 詹姆斯·威尔逊

如果使用 preserveChanges=false targetDataset.Merge(sourceDataset) 对上表调用 Merge 方法,会得到以下数据:

DataRowVersion 目标数据集 源数据集
原版 詹姆斯·威尔逊 詹姆斯·威尔逊
当前 詹姆斯·威尔逊 詹姆斯·威尔逊

调用Merge方法并传入preserveChanges = true targetDataset.Merge(sourceDataset, true)后,结果为以下数据:

DataRowVersion 目标数据集 源数据集
原版 詹姆斯·威尔逊 詹姆斯·威尔逊
当前 吉姆·威尔逊 詹姆斯·威尔逊

谨慎

在这种情况下 preserveChanges = true ,如果在 RejectChanges 目标数据集中的记录上调用该方法,则该方法会还原到 数据集中的原始数据。 这意味着,如果尝试使用目标数据集更新原始数据源,则可能无法找到要更新的原始行。 防止并发冲突的方法:使用数据源中已更新的记录填充另一个数据集,然后执行合并来防止发生并发冲突。 (如果其他用户在填充数据集后修改数据源中的记录,则会发生并发冲突。

更新约束

若要更改现有数据行,请在各个列中添加或更新数据。 如果数据集包含约束(如外键或不可为 null 的约束),则记录可能在更新时暂时处于错误状态。 也就是说,在完成一列的更新之后和到达下一列前之间,它可能处于错误状态。

若要防止过早违反约束,可以暂时挂起更新约束。 这有两个用途:

  • 防止在完成更新一列但尚未开始更新其他列时引发错误。

  • 它可防止引发某些更新事件(通常用于验证的事件)。

注释

在 Windows 窗体中,数据网格中内置的数据绑定架构会暂停约束检查,直至焦点离开某一数据行,因此不需要显式调用 BeginEditEndEditCancelEdit 方法。

在数据集上调用Merge 方法时,将自动禁用约束条件。 合并完成后,如果数据集上的某些约束无法启用,则会抛出 ConstraintException。 在这种情况下,属性 EnforceConstraints 设置为 false, ,并且必须在将属性重置 EnforceConstraintstrue之前解决所有约束冲突。

完成更新后,您可以重新启用约束检查,这也会重新启用更新事件并触发这些事件。

有关暂停事件的更多信息,请参阅填充数据集时关闭约束

数据集更新错误

更新数据集中的记录时,可能会出现错误。 例如,你可能无意中将错误类型的数据写入列,或者数据太长,或者存在其他完整性问题的数据。 或者,你可能拥有特定于应用程序的验证检查,这些检查可能在更新事件的任何阶段触发自定义错误。 有关详细信息,请参阅 验证数据集中的数据

维护有关更改的信息

数据集中的更改信息通过两种方式进行维护:标记指示已更改的行,RowState以及保留记录的多个副本(DataRowVersion)。 通过使用此信息,进程可以确定数据集中已更改的内容,并且可以向数据源发送相应的更新。

RowState 属性

对象的 RowState 属性 DataRow 是一个值,该值提供有关特定数据行的状态的信息。

下表详细介绍了枚举的 DataRowState 可能值:

DataRowState 值 DESCRIPTION
Added 该行已作为项添加到 DataRowCollection。 (此状态下的行没有相应的原始版本,因为调用最后 AcceptChanges 一个方法时不存在该行)。
Deleted 该行是使用 DataRow 对象的 Delete 删除的。
Detached 该行已创建,但不是任何 DataRowCollection行的一部分。 对象 DataRow 在创建后、添加到集合之前以及从集合中删除对象后立即处于此状态。
Modified 行中的列值以某种方式更改。
Unchanged 自从上次调用AcceptChanges以来, 该行尚未更改。

DataRowVersion 枚举

数据集维护多个版本的记录。 DataRowVersion字段用于在使用Item[]属性或GetChildRows方法从DataRow对象中检索DataRow的值时。

下表详细介绍了枚举的 DataRowVersion 可能值:

DataRowVersion 值 DESCRIPTION
Current 记录的当前版本包含自上次调用以来 AcceptChanges 对记录执行的所有修改。 如果该行已删除,则没有当前版本。
Default 记录的默认值,由数据集架构或数据源定义。
Original 记录的原始版本是记录的副本,因为它是上次在数据集中提交更改时的副本。 实际上,这通常是从数据源读取记录的版本。
Proposed 在更新期间(即调用 BeginEdit 方法与 EndEdit 方法之间)暂时可用的记录的建议版本。 您通常在处理像 RowChanging 这样的事件的处理程序中访问记录的建议版本。 调用 CancelEdit 方法将撤销更改,并删除数据行的建议版本。

将更新信息传输到数据源时,原始版本和当前版本非常有用。 通常,将更新发送到数据源时,数据库的新信息位于记录的当前版本中。 原始版本中的信息用于查找要更新的记录。

例如,如果记录的主键已更改,则需要一种方法来查找数据源中的正确记录以更新更改。 如果不存在原始版本,那么该记录很可能会被添加到数据源中,这不仅会导致多出一条不需要的记录,还会产生一条不准确且过时的记录。 这两个版本也用于并发控制。 可以将原始版本与数据源中的记录进行比较,以确定记录自加载到数据集后是否发生了更改。

在实际将更改提交到数据集之前需要执行验证,此时建议的版本非常有用。

即使记录已更改,该行并不总是有原始版本或当前版本。 在表中插入新行时,没有原始版本,只有当前版本。 同样,如果通过调用表 Delete 的方法删除行,则存在原始版本,但没有当前版本。

可以通过查询数据行 HasVersion 的方法来测试是否存在特定版本的记录。 在请求列的值时,可以通过将枚举值DataRowVersion作为可选参数,来访问记录的任一版本。

获取已更改的记录

通常不要更新数据集中的每个记录。 例如,用户可能正在使用显示许多记录的 Windows 窗体 DataGridView 控件。 但是,用户可能只更新几个记录,删除一条,然后插入一个新记录。 数据集和数据表提供了一种方法(GetChanges)用于仅返回已修改的行。

您可以使用数据表的方法GetChanges或数据集本身的方法GetChanges来创建已更改记录的子集GetChanges。 如果调用数据表的方法,它将返回仅包含已更改记录的表的副本。 同样,如果在数据集上调用该方法,则会收到一个新数据集,其中包含仅更改的记录。

GetChanges 本身返回所有已更改的记录。 相比之下,通过将所需的 DataRowState 参数传递给 GetChanges 方法,可以指定所需的更改记录的子集:新添加的记录、标记为删除、分离记录或修改的记录。

如果要将记录发送到另一个组件进行处理,获取已更改记录的子集非常有用。 通过仅获取组件所需的记录,可以减少与其他组件的通信开销,而不是发送整个数据集。

提交对数据集的更改

在数据集中进行更改时, RowState 将设置更改行的属性。 记录的原始版本和当前版本由RowVersion属性建立、维护,并提供给您。 这些已更改行属性中存储的元数据对于向数据源发送正确的更新是必需的。

如果更改反映数据源的当前状态,则不再需要维护此信息。 通常情况下,数据集及其源有两次同步的机会:

  • 在将信息加载到数据集之后(例如从数据源读取数据时)会立刻同步。

  • 在将更改从数据集发送到数据源之后(但不是在此之前,在之前同步会使你丢失将更改发送到数据库所需的更改信息)会同步。

通过调用AcceptChanges方法,可以将挂起的更改提交到数据集。 通常, AcceptChanges 在以下情况下调用:

  • 加载数据集后。 如果通过调用 TableAdapter Fill 的方法加载数据集,适配器会自动为你提交更改。 但是,如果通过将另一个数据集合并到其中来加载数据集,则必须手动提交更改。

    注释

    通过将适配器的AcceptChangesDuringFill属性设置为false,可以在调用Fill方法时阻止适配器自动提交更改。 如果将false设置为,则在填充过程中插入的每一行的RowState将设置为Added

  • 将数据集更改发送到另一个进程(如 XML Web 服务)之后。

    谨慎

    以这种方式提交更改会清除任何更改信息。 在执行要求应用程序知道数据集中进行了哪些更改的操作之前,请勿提交更改。

此方法可实现以下目的:

该方法 AcceptChanges 在三个级别可用。 可在 DataRow 对象上调用该方法,仅提交该行的更改。 可以在对象 DataTable 上调用它,以提交表中的所有行。 最后,可在 DataSet 对象上调用该方法,以提交数据集所有表的所有记录中所有挂起的更改。

下表描述了基于调用方法的对象提交哪些更改:

方法 结果
System.Data.DataRow.AcceptChanges 仅提交特定行上的更改。
System.Data.DataTable.AcceptChanges 提交特定表中所有行上的更改。
System.Data.DataSet.AcceptChanges 提交数据集的所有表的所有行上的更改。

注释

如果通过调用 TableAdapter Fill 的方法加载数据集,则无需显式接受更改。 默认情况下, Fill 该方法在完成填充数据表后调用 AcceptChanges 该方法。

RejectChanges 是一个相关方法,该方法将记录的 Original 版本复制回 Current 版本,从而达到撤消更改的效果。 它还会将每个记录的RowState设置回Unchanged

数据验证

为了验证应用程序中的数据是否满足传递给它的进程的要求,通常需要添加验证。 这可能涉及检查表单中的用户条目是否正确、验证由另一个应用程序发送到应用程序的数据,甚至检查组件中计算的信息是否属于数据源和应用程序要求的约束。

可以通过多种方式验证数据:

  • 在业务层中,通过将代码添加到应用程序来验证数据。 数据集是您可以执行这些操作的一个平台。 数据集提供了后端验证的一些优势,例如,在列值和行值发生更改时验证更改的能力。 有关详细信息,请参阅 验证数据集中的数据

  • 在呈现层中,通过向表单添加验证。 有关详细信息,请参阅 Windows 窗体中的用户输入验证

  • 在数据后端,将数据发送到数据源(例如数据库),并允许它接受或拒绝数据。 如果使用的数据库具有用于验证数据和提供错误信息的复杂设施,则可以采用一种实际方法,因为无论数据来自何处,都可以验证数据。 但是,此方法可能不符合特定于应用程序的验证要求。 此外,让数据源验证数据可能会导致对数据源进行多次往返,具体取决于应用程序如何促进后端引发的验证错误的解决。

    重要

    当使用数据命令且CommandType属性被设置为Text时,在将信息传递给数据库之前,请仔细检查从客户端发送的信息。 恶意用户可能会尝试发送已修改的 SQL 语句或其他 SQL 语句,从而获得未经授权的访问或损坏数据库。 将用户输入传输到数据库之前,请始终验证信息是否有效。 最佳做法是尽可能始终使用参数化查询或存储过程。

将更新传输到数据源

在数据集中进行更改后,可以将更改传输到数据源。 通常,可以通过调用 TableAdapter(或数据适配器)的方法来完成此操作。 该方法循环访问数据表中的每个记录,确定需要哪种类型的更新(更新、插入或删除),然后运行相应的命令。

作为说明更新如何进行的示例,假设您的应用程序使用一个包含单个数据表的数据集。 应用程序从数据库提取两行。 检索后,内存中数据表如下所示:

(RowState)     CustomerID   Name             Status
(Unchanged)    c200         Robert Lyon      Good
(Unchanged)    c400         Nancy Buchanan    Pending

应用程序将 Nancy Buchanan 的状态更改为“首选”。由于此更改,该行的属性RowState的值将Modified更改为 Unchanged 。 第一行的属性 RowState 值保持不变 Unchanged。 数据表现在如下所示:

(RowState)     CustomerID   Name             Status
(Unchanged)    c200         Robert Lyon      Good
(Modified)     c400         Nancy Buchanan    Preferred

应用程序现在调用 Update 该方法将数据集传输到数据库。 该方法依次检查每一行。 对于第一行,该方法不会将 SQL 语句传输到数据库,因为该行自最初从数据库提取以来未更改。

但是,对于第二行,该方法 Update 会自动调用正确的数据命令并将其传输到数据库。 SQL 语句的特定语法取决于基础数据存储支持的 SQL 方言。 但是,传输的 SQL 语句的以下一般特征值得注意:

  • 传输的 SQL 语句是一个 UPDATE 语句。 适配器知道使用 UPDATE 语句,因为属性的值 RowStateModified

  • 传输的 SQL 语句包含一个 WHERE 子句,指示语句的目标 UPDATE 是其中行 CustomerID = 'c400'。 该语句的 SELECT 这一部分将目标行与其他所有行区分开来,因为 CustomerID 该行是目标表的主键。 WHERE 子句的信息派生自记录的原始版本 (DataRowVersion.Original),以防标识行所需的值发生了更改。

  • 传输的 SQL 语句包括子 SET 句,用于设置修改列的新值。

    注释

    如果 TableAdapter 的属性 UpdateCommand 已设置为存储过程的名称,则适配器不会构造 SQL 语句。 而是使用传入的相应参数调用存储过程。

传递参数

通常使用参数传递要在数据库中更新的记录的值。 当 TableAdapter Update 的方法运行语句 UPDATE 时,它需要填充参数值。 它从 Parameters 集合中获取适用于数据命令的这些值;在本例中,是 TableAdapter 中的 UpdateCommand 对象。

如果使用 Visual Studio 工具生成数据适配器,则 UpdateCommand 对象包含与语句中的每个参数占位符对应的参数集合。

每个 System.Data.SqlClient.SqlParameter.SourceColumn 参数的属性指向数据表中的列。 例如,参数SourceColumn的属性au_idOriginal_au_id设置为数据表中包含作者 ID 的任何列。Update适配器的方法运行时,它会从要更新的记录中读取作者 ID 列,并将值填充到语句中。

UPDATE在语句中,需要指定新值(这些值将写入记录)和旧值(以便记录可以位于数据库中)。 因此,每个值有两个参数:一个用于 SET 子句,另一个用于 WHERE 子句。 这两个参数从要更新的记录中读取数据,但它们根据参数 SourceVersion 的属性获取不同版本的列值。 子句的参数 SET 获取当前版本,子句的参数 WHERE 获取原始版本。

注释

还可以在代码中自行设置集合中的 Parameters 值,通常在数据适配器事件的 RowChanging 事件处理程序中执行此作。