处理 .NET Framework 数据库应用程序中的并发异常

注意

数据集和相关类是 2000 年代初的旧 .NET Framework 技术,使应用程序能够在应用程序与数据库断开连接时处理内存中的数据。 这些方法对于使用户能够修改数据并持续更改回数据库的应用程序特别有用。 虽然数据集已被证明是一项非常成功的技术,但我们建议新的 .NET 应用程序使用 Entity Framework Core。 实体框架提供了一种更自然的方式来将表格数据作为对象模型,并且具有更简单的编程接口。

在当两个用户同时尝试更改数据库中的相同数据时,将引发并发异常 (System.Data.DBConcurrencyException)。 本演练将创建一个 Windows 应用程序,该应用程序演示如何捕获 DBConcurrencyException,找导致错误的行,并了解处理该错误的策略。

本演练将指导你完成以下过程:

  1. 创建新的“Windows 窗体应用 (.NET Framework)”项目

  2. 基于 Northwind Customers 表创建新的数据集。

  3. 使用 DataGridView 创建窗体以显示数据。

  4. 使用 Northwind 数据库中的 Customers 表中的数据填充数据集。

  5. 使用服务器资源管理器中的“显示表数据”功能,访问 Customers 表的数据并更改一条记录。

  6. 将同一记录更改为其他值,更新数据集,并尝试将更改写入数据库,这导致引发并发错误。

  7. 捕获该错误,然后显示该记录的不同版本,允许用户确定是继续更新数据库还是取消更新。

先决条件

本演练使用 SQL Server Express LocalDB 和 Northwind 示例数据库。

  1. 如果尚未安装 SQL Server Express LocalDB,可以从 SQL Server Express 下载页或通过 Visual Studio 安装程序安装。 在 Visual Studio 安装程序中,可以将 SQL Server Express LocalDB 作为数据存储和处理工作负载的一部分或作为单个组件进行安装。

  2. 按照以下步骤安装 Northwind 示例数据库:

    1. 在 Visual Studio 中,打开“SQL Server 对象资源管理器”窗口。 (在 Visual Studio 安装程序中 SQL Server 对象资源管理器作为数据存储和处理工作负载的一部分安装。)展开 SQL Server 节点。 右键单击 LocalDB 实例并选择“新建查询”。

      此时将打开查询编辑器窗口。

    2. Northwind Transact-SQL 脚本复制到剪贴板。 此 T-SQL 脚本从头开始创建 Northwind 数据库并用数据填充它。

    3. 将 T-SQL 脚本粘贴到查询编辑器中,然后选择“执行”按钮。

      不久后,查询完成运行并且 Northwind 数据库创建完成。

创建新项目

从创建新的 Windows 窗体应用程序开始:

  1. 在 Visual Studio 的“文件”菜单中,依次选择“新建”>“项目” 。

  2. 在左侧窗格中展开 Visual C#Visual Basic,然后选择 Windows 桌面

  3. 在中间窗格中,选择“Windows 窗体应用”项目类型。

  4. 将项目命名为“ConcurrencyWalkthrough”,然后选择“确定” 。

    将创建 ConcurrencyWalkthrough 项目并将其添加到解决方案资源管理器,并在设计器中打开新窗体。

创建 Northwind 数据集

接下来,创建一个名为 NorthwindDataSet 的数据集:

  1. 在“数据”菜单上,选择“添加新数据源”。

    “数据源配置”向导随即打开。

  2. 在“选择数据源类型”屏幕上,选择“数据库”。

    Visual Studio 中的数据源配置向导

  3. 从可用连接列表中选择与 Northwind 示例数据库的连接。 如果该连接不在连接列表中,请选择“新建连接”。

    注意

    如果要连接到本地数据库文件,请在系统询问是否将文件添加到项目时选择“否”。

  4. 在“将连接字符串保存到应用程序配置文件”屏幕中,选择“下一步” 。

  5. 展开“Tables”节点,然后选择“Customers”表。 数据集的默认名称应为 NorthwindDataSet。

  6. 选择“完成”,将数据集添加到项目。

创建数据绑定 DataGridView 控件

在本部分中,你要将“Customers”项通过从“数据源”窗口拖动到 Windows 窗体来创建 System.Windows.Forms.DataGridView

  1. 若要打开“数据源”窗口,在“数据”菜单上选择“显示数据源” 。

  2. 在“数据源”窗口中,展开“NorthwindDataSet”节点,然后选择“Customers”表。

  3. 选择表节点上的向下箭头,然后在下拉列表中选择“DataGridView”。

  4. 将表拖到窗体上的空白区域。

    名为 CustomersDataGridView 的 DataGridView 控件和名为 CustomersBindingNavigator 的 BindingNavigator 控件将添加到绑定到 BindingSource 的窗体中。 而这又绑定到 NorthwindDataSet 中的 Customers 表。

测试窗体

现在可以测试窗体,以确保它的行为迄今为止符合预期:

  1. 选择 F5 运行该应用程序。

    将会显示窗体,其中有一个 DataGridView 控件,该控件用 Customers 表中的数据填充。

  2. 在“调试”菜单中,选择“停止调试”

处理并发错误

如何处理错误取决于用于管控应用程序的特定业务规则。 对于本演练,我们将使用以下策略作为如何处理并发错误的示例。

应用程序向用户显示记录的三个版本:

  • 数据库中的当前记录

  • 加载到数据集中的原始记录

  • 数据集中建议的更改

然后,用户可以使用建议的版本覆盖数据库,或者取消更新,然后使用数据库中的新值刷新数据集。

若要启用并发错误处理

  1. 创建自定义错误处理程序。

  2. 向用户显示选项。

  3. 处理用户的响应。

  4. 重新发送更新,或重置数据集中的数据。

添加代码以处理并发异常

尝试执行更新并引发异常时,通常需要利用所引发异常提供的信息执行某些操作。 在本部分,你将添加尝试更新数据库的代码。 还要处理可能会引发的任何 DBConcurrencyException 以及任何其他异常。

注意

稍后将在本演练中添加 CreateMessageProcessDialogResults 方法。

  1. Form1_Load 方法下面添加以下代码:

    private void UpdateDatabase()
    {
        try
        {
            this.customersTableAdapter.Update(this.northwindDataSet.Customers);
            MessageBox.Show("Update successful");
        }
        catch (DBConcurrencyException dbcx)
        {
            DialogResult response = MessageBox.Show(CreateMessage((NorthwindDataSet.CustomersRow)
                (dbcx.Row)), "Concurrency Exception", MessageBoxButtons.YesNo);
    
            ProcessDialogResult(response);
        }
        catch (Exception ex)
        {
            MessageBox.Show("An error was thrown while attempting to update the database.");
        }
    }
    

  1. 替换 CustomersBindingNavigatorSaveItem_Click 方法,使其调用 UpdateDatabase 方法,如下所示:

    private void customersBindingNavigatorSaveItem_Click(object sender, EventArgs e)
    {
        UpdateDatabase();
    }
    

向用户显示选项

刚编写的代码调用 CreateMessage 过程,以向用户显示错误信息。 对于本演练,你将使用消息框向用户显示记录的不同版本。 这使用户可以选择是使用更改覆盖记录还是取消编辑。 用户在消息框上选择选项(单击按钮)后,响应将传递给 ProcessDialogResult 方法。

通过将以下代码添加到代码编辑器来创建消息。 在 UpdateDatabase 方法下输入以下代码:

private string CreateMessage(NorthwindDataSet.CustomersRow cr)
{
    return
        "Database: " + GetRowData(GetCurrentRowInDB(cr), DataRowVersion.Default) + "\n" +
        "Original: " + GetRowData(cr, DataRowVersion.Original) + "\n" +
        "Proposed: " + GetRowData(cr, DataRowVersion.Current) + "\n" +
        "Do you still want to update the database with the proposed value?";
}


//--------------------------------------------------------------------------
// This method loads a temporary table with current records from the database
// and returns the current values from the row that caused the exception.
//--------------------------------------------------------------------------
private NorthwindDataSet.CustomersDataTable tempCustomersDataTable = 
    new NorthwindDataSet.CustomersDataTable();

private NorthwindDataSet.CustomersRow GetCurrentRowInDB(NorthwindDataSet.CustomersRow RowWithError)
{
    this.customersTableAdapter.Fill(tempCustomersDataTable);

    NorthwindDataSet.CustomersRow currentRowInDb = 
        tempCustomersDataTable.FindByCustomerID(RowWithError.CustomerID);

    return currentRowInDb;
}


//--------------------------------------------------------------------------
// This method takes a CustomersRow and RowVersion 
// and returns a string of column values to display to the user.
//--------------------------------------------------------------------------
private string GetRowData(NorthwindDataSet.CustomersRow custRow, DataRowVersion RowVersion)
{
    string rowData = "";

    for (int i = 0; i < custRow.ItemArray.Length ; i++ )
    {
        rowData = rowData + custRow[i, RowVersion].ToString() + " ";
    }
    return rowData;
}

处理用户的响应

还需要代码来处理用户对消息框的响应。 可以选择使用建议的更改覆盖数据库中的当前记录,或者放弃本地更改,然后使用数据库中当前的记录刷新数据表。 如果用户选择“是”,将调用 Merge 方法,同时将 preserveChanges 参数设置为 true。 这会使得更新尝试成功,因为记录的原始版本现在与数据库中的记录匹配。

在上一部分中添加的代码下方添加以下代码:

// This method takes the DialogResult selected by the user and updates the database 
// with the new values or cancels the update and resets the Customers table 
// (in the dataset) with the values currently in the database.

private void ProcessDialogResult(DialogResult response)
{
    switch (response)
    {
        case DialogResult.Yes:
            northwindDataSet.Merge(tempCustomersDataTable, true, MissingSchemaAction.Ignore);
            UpdateDatabase();
            break;

        case DialogResult.No:
            northwindDataSet.Merge(tempCustomersDataTable);
            MessageBox.Show("Update cancelled");
            break;
    }
}

测试窗体行为

现在可以测试窗体,以确保它的行为符合预期。 若要模拟并发冲突,请在填充 NorthwindDataSet 后更改数据库中的数据。

  1. 选择 F5 运行该应用程序。

  2. 窗体出现后,让窗体保持运行状态并切换到 Visual Studio IDE。

  3. 在“视图”菜单中,选择“服务器资源管理器”。

  4. 在服务器资源管理器中,展开你的应用程序使用的连接,然后展开“Tables”节点 。

  5. 右键单击“Customers”表,然后选择“显示表数据”。

  6. 在第一条记录 (ALFKI) 中,将 ContactName 改为“Maria Anders2”。

    注意

    导航到其他行以提交更改。

  7. 切换到 ConcurrencyWalkthrough 的运行窗体。

  8. 在表单上的第一条记录 (ALFKI) 中,将 ContactName 改为“Maria Anders1”。

  9. 选择“保存”按钮。

    将引发并发错误,并显示消息框。

    选择“否”将取消更新并用数据库中当前的值更新数据集。 选择“是”会将建议的值写入数据库。