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

注释

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

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

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

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

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

  3. 创建包含 DataGridView 用于显示数据的窗体。

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

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

  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. 展开 “表” 节点并选择 “客户 ”表。 数据集的默认名称应为 NorthwindDataSet

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

创建数据绑定的 DataGridView 控件

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

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

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

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

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

    DataGridView名为CustomersDataGridView的控件和BindingNavigator名为CustomersBindingNavigator的控件已添加到绑定到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;
}

处理用户的响应

还需要代码来处理用户对消息框的响应。 这些选项是用建议的更改覆盖数据库中的当前记录,或者放弃本地更改,并使用数据库中当前记录刷新数据表。 如果用户选择 “是”,则调用该方法,并将 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. 服务器资源管理器中,展开应用程序正在使用的连接,然后展开 “表” 节点。

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

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

    注释

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

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

  8. 在窗体的第一条记录(ALFKI)中,将 ContactName 更改为 Maria Anders1

  9. 选择保存按钮。

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

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