使用 SqlDataSource 实现乐观并发 (C#)

作者 :Scott Mitchell

下载 PDF

在本教程中,我们将回顾乐观并发控制的基本原理,然后探索如何使用 SqlDataSource 控件实现它。

简介

在前面的教程中,我们了解了如何向 SqlDataSource 控件添加插入、更新和删除功能。 简而言之,为了提供这些功能,我们需要在控件的 、 或 属性中指定相应的 INSERTUpdateCommandDeleteCommand SQL 语句,以及 、 UpdateParametersDeleteParameters 集合中的InsertParameters相应参数。DELETEUPDATEInsertCommand 虽然可以手动指定这些属性和集合,但“配置数据源”向导的“高级”按钮提供了“生成 INSERTUPDATEDELETE 语句”复选框,该复选框将基于 SELECT 语句自动创建这些语句。

除了“生成 INSERTUPDATEDELETE 语句”复选框外,“高级 SQL 生成选项”对话框还包括“使用乐观并发”选项 (请参阅图 1) 。 选中后, WHERE 自动生成 UPDATE 的 和 DELETE 语句中的 子句将修改为仅执行更新或删除,前提是自用户上次将数据加载到网格后尚未修改基础数据库数据。

可以从“高级 SQL 生成选项”对话框添加乐观并发支持

图 1:可以从“高级 SQL 生成选项”对话框添加乐观并发支持

实现乐观并发 教程中,我们回顾了乐观并发控制的基础知识以及如何将其添加到 ObjectDataSource。 在本教程中,我们将修改乐观并发控制的基本原理,然后探索如何使用 SqlDataSource 实现它。

乐观并发回顾

对于允许多个同时编辑或删除相同数据的 Web 应用程序,一个用户可能会意外覆盖另一个更改。 在 实现乐观并发 教程中,我提供了以下示例:

假设两位用户 Jisun 和 Sam 都访问了允许访问者通过 GridView 控件更新和删除产品的应用程序中的页面。 两者单击 Chai 的“编辑”按钮大约在同一时间。 Jisun 将产品名称更改为柴茶,然后单击“更新”按钮。 最终结果是发送到 UPDATE 数据库的语句,该语句将 产品的所有 可更新字段设置为 (即使 Jisun 只更新了一个字段, ProductName) 。 此时,数据库具有此特定产品的柴茶、饮料类别、供应商异国液体等值。 但是,Sam 屏幕上的 GridView 仍会将可编辑 GridView 行中的产品名称显示为 Chai。 提交 Jisun 更改几秒钟后,Sam 会将类别更新为“调味品”,然后单击“更新”。 这会导致向数据库发送一条 UPDATE 语句,该语句将产品名称设置为 Chai,将 CategoryID 设置为相应的调味品类别 ID,依此类举。 已覆盖对产品名称的 Jisun 更改。

图 2 演示了这种交互。

当两个用户同时更新记录时,一个用户的更改可能会覆盖另一个用户

图 2:当两个用户同时更新记录时,一个用户的更改可能会覆盖其他 (单击以查看全尺寸图像)

若要防止此方案展开,必须实现一种 并发控制 形式。 乐观并发 本教程的重点是以下假设:虽然时不时会出现并发冲突,但绝大多数情况下不会出现此类冲突。 因此,如果确实发生冲突,乐观并发控制只会通知用户无法保存其更改,因为其他用户修改了相同的数据。

注意

对于假定存在许多并发冲突的应用程序,或者如果此类冲突不可容忍,则可以改用悲观并发控制。 有关悲观并发控制的更全面讨论,请参阅 实现乐观 并发教程。

乐观并发控制的工作原理是确保正在更新或删除的记录具有与更新或删除进程启动时相同的值。 例如,在可编辑的 GridView 中单击“编辑”按钮时,将从数据库中读取记录值,并显示在 TextBoxes 和其他 Web 控件中。 这些原始值由 GridView 保存。 稍后,在用户进行更改并单击“更新”按钮后, UPDATE 所使用的语句必须考虑原始值加上新值,并且仅当用户开始编辑的原始值与仍在数据库中的值相同时,才更新基础数据库记录。 图 3 描述了此事件序列。

若要成功更新或删除,原始值必须等于当前数据库值

图 3:若要成功更新或删除,原始值必须等于当前数据库值 (单击以查看全尺寸图像)

有多种方法可以实现乐观并发 (请参阅 Peter A. Bromberg乐观并发更新逻辑 ,简要了解) 的多种选项。 SqlDataSource (以及数据访问层) 中使用的 ADO.NET 类型化数据集所使用的技术扩充 WHERE 了 子句,以包含所有原始值的比较。 例如,下面的 UPDATE 语句仅当当前数据库值等于更新 GridView 中的记录时最初检索到的值时,才会更新产品的名称和价格。 @ProductName@UnitPrice 参数包含用户输入的新值,而 @original_ProductName@original_UnitPrice 包含最初在单击“编辑”按钮时加载到 GridView 中的值:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

如本教程中所示,使用 SqlDataSource 启用乐观并发控制就像选中复选框一样简单。

步骤 1:创建支持乐观并发的 SqlDataSource

首先从 SqlDataSource 文件夹中打开OptimisticConcurrency.aspx页面。 将 SqlDataSource 控件从“工具箱”拖到Designer,将其 ID 属性设置为 ProductsDataSourceWithOptimisticConcurrency。 接下来,单击控件智能标记中的“配置数据源”链接。 在向导的第一个屏幕中,选择使用 NORTHWINDConnectionString ,然后单击“下一步”。

选择使用 NORTHWINDConnectionString

图 4:选择使用 NORTHWINDConnectionString (单击以查看全尺寸图像)

对于此示例,我们将添加一个 GridView,使用户能够编辑 Products 表。 因此,从“配置 Select 语句”屏幕中,从下拉列表中选择 Products 表,然后选择 ProductIDProductNameUnitPriceDiscontinued 列,如图 5 所示。

从“产品”表中,返回 ProductID、ProductName、UnitPrice 和已停用列

图 5:在 Products 表中,返回 ProductIDProductNameUnitPriceDiscontinued 列 (单击以查看全尺寸图像)

选取列后,单击“高级”按钮打开“高级 SQL 生成选项”对话框。 选中“生成 INSERTUPDATEDELETE 语句”和“使用乐观并发”复选框,然后单击“确定” (有关屏幕截图) 的屏幕截图,请参阅图 1。 单击“下一步”,然后单击“完成”,完成向导。

完成“配置数据源”向导后,请花点时间检查生成的 DeleteCommandUpdateCommand 属性以及 DeleteParametersUpdateParameters 集合。 执行此操作的最简单方法是单击左下角的“源”选项卡,查看页面声明性语法。 可在此处找到 UpdateCommand 值:

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued

集合中具有七个 UpdateParameters 参数:

<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ...>
    <DeleteParameters>
      ...
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </UpdateParameters>
    ...
</asp:SqlDataSource>

同样, DeleteCommand 属性和 DeleteParameters 集合应如下所示:

DELETE FROM [Products]
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued
<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ...>
    <DeleteParameters>
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        ...
    </UpdateParameters>
    ...
</asp:SqlDataSource>

除了增加 WHEREDeleteCommand 属性的子句 UpdateCommand (并将其他参数添加到相应的参数集合) ,选择“使用乐观并发”选项会调整另外两个属性:

当数据 Web 控件调用 SqlDataSource Update()Delete() 方法时,它将传入原始值。 如果 SqlDataSource 的 ConflictDetection 属性设置为 CompareAllValues,则这些原始值将添加到 命令中。 属性 OldValuesParameterFormatString 提供用于这些原始值参数的命名模式。 “配置数据源”向导使用 original_{0}并相应地命名 和 DeleteCommand 属性和UpdateParametersDeleteParameters集合中的每个UpdateCommand原始参数。

注意

由于未使用 SqlDataSource 控件的插入功能,因此可以随意删除 InsertCommand 属性及其 InsertParameters 集合。

正确处理NULL

遗憾的是,使用乐观并发时,由“配置数据源”向导自动生成的扩充 UPDATEDELETE 语句 不适用于 包含 NULL 值的记录。 若要了解原因,请考虑 SqlDataSource :UpdateCommand

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued

UnitPrice表中的Products列可以有NULL值。 如果特定记录具有 NULL 的值 UnitPrice,则 WHERE 子句部分 [UnitPrice] = @original_UnitPrice 的计算结果 将始终 为 False,因为 NULL = NULL 始终返回 False。 因此,无法编辑或删除包含 NULL 值的记录,因为 UPDATEDELETE 语句 WHERE 子句不会返回任何要更新或删除的行。

注意

此 bug 于 2004 年 6 月首次在 SqlDataSource 生成不正确的 SQL 语句 中报告给 Microsoft,并计划在下一版本的 ASP.NET 中修复。

若要解决此问题,必须手动更新 WHERE 可以具有NULL的所有列的 和 DeleteCommand 属性中的 UpdateCommand 子句。 一般情况下,请更改为 [ColumnName] = @original_ColumnName

(
   ([ColumnName] IS NULL AND @original_ColumnName IS NULL)
     OR
   ([ColumnName] = @original_ColumnName)
)

可以直接通过声明性标记、属性窗口中的 UpdateQuery 或 DeleteQuery 选项,或通过“配置数据源”向导中的“指定自定义 SQL 语句或存储过程”选项中的 UPDATE 和 DELETE 选项卡进行此修改。 同样,必须对 可以包含NULL值的 和 DeleteCommandWHERE中的每个UpdateCommand列进行此修改。

将此应用于我们的示例会导致以下修改 UpdateCommandDeleteCommand 值:

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
        OR ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued
DELETE FROM [Products]
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
        OR ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued

步骤 2:使用“编辑”和“删除”选项添加 GridView

将 SqlDataSource 配置为支持乐观并发后,剩下的就是将数据 Web 控件添加到利用此并发控件的页面。 在本教程中,让我们添加一个提供编辑和删除功能的 GridView。 为此,请将 GridView 从工具箱拖到Designer并将其设置为 IDProducts。 从 GridView 智能标记中,将其绑定到步骤 1 中添加的 ProductsDataSourceWithOptimisticConcurrency SqlDataSource 控件。 最后,检查智能标记中的“启用编辑”和“启用删除”选项。

将 GridView 绑定到 SqlDataSource 并启用编辑和删除

图 6:将 GridView 绑定到 SqlDataSource 并启用编辑和删除 (单击以查看全尺寸图像)

添加 GridView 后,通过删除 ProductID BoundField、将 BoundField 的 HeaderText 属性更改为 ProductName Product 并更新 UnitPrice BoundField 以便其属性只是 Price 来配置其HeaderText外观。 理想情况下,我们会增强编辑界面,以便为 ProductName 值添加 RequiredFieldValidator,为值添加 CompareValidator UnitPrice (,以确保它是格式正确的数值) 。 有关自定义 GridView 编辑界面的更深入介绍,请参阅 自定义数据修改 接口教程。

注意

必须启用 GridView 视图状态,因为从 GridView 传递到 SqlDataSource 的原始值存储在视图状态中。

对 GridView 进行这些修改后,GridView 和 SqlDataSource 声明性标记应如下所示:

<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ConflictDetection="CompareAllValues"
    ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
    DeleteCommand=
        "DELETE FROM [Products]
         WHERE [ProductID] = @original_ProductID
         AND [ProductName] = @original_ProductName
         AND (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
              OR ([UnitPrice] = @original_UnitPrice))
         AND [Discontinued] = @original_Discontinued"
    OldValuesParameterFormatString=
        "original_{0}"
    SelectCommand=
        "SELECT [ProductID], [ProductName], [UnitPrice], [Discontinued]
         FROM [Products]"
    UpdateCommand=
        "UPDATE [Products]
         SET [ProductName] = @ProductName, [UnitPrice] = @UnitPrice,
            [Discontinued] = @Discontinued
         WHERE [ProductID] = @original_ProductID
         AND [ProductName] = @original_ProductName
         AND (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
            OR ([UnitPrice] = @original_UnitPrice))
        AND [Discontinued] = @original_Discontinued">
    <DeleteParameters>
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </UpdateParameters>
</asp:SqlDataSource>
<asp:GridView ID="Products" runat="server"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ProductsDataSourceWithOptimisticConcurrency">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="UnitPrice" HeaderText="Price"
            SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

若要查看操作中的乐观并发控制,请打开两个浏览器窗口,并在两个窗口中加载 OptimisticConcurrency.aspx 页面。 在两个浏览器中单击第一个产品的“编辑”按钮。 在一个浏览器中,更改产品名称并单击“更新”。 浏览器将回发,GridView 将返回到其预编辑模式,显示刚刚编辑的记录的新产品名称。

在第二个浏览器窗口中,更改价格 (但将产品名称保留为其原始值) 并单击“更新”。 回发时,网格将返回到其预编辑模式,但不会记录价格的变化。 第二个浏览器显示与第一个具有旧价格的新产品名称相同的值。 第二个浏览器窗口中所做的更改已丢失。 此外,更改会相当安静地丢失,因为没有异常或消息指示刚发生并发冲突。

第二个浏览器窗口中的更改已无提示丢失

图 7:第二个浏览器窗口中的更改无提示丢失 (单击以查看全尺寸图像)

未提交 UPDATE 第二个浏览器更改的原因是语句 s WHERE 子句筛选掉了所有记录,因此不会影响任何行。 让我们再次看一下 语句 UPDATE

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL) OR
        ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued

当第二个浏览器窗口更新记录时,子句中指定的 WHERE 原始产品名称与现有产品名称 (不匹配,因为它由第一个浏览器) 更改。 因此,语句 [ProductName] = @original_ProductName 返回 False,并且 UPDATE 不会影响任何记录。

注意

删除的工作方式相同。 打开两个浏览器窗口后,首先使用一个浏览器窗口编辑给定产品,然后保存其更改。 在一个浏览器中保存更改后,单击另一个浏览器中同一产品的“删除”按钮。 由于原始值在 语句 s WHERE 子句中DELETE不匹配,因此删除以无提示方式失败。

从最终用户的角度来看,第二个浏览器窗口中,单击“更新”按钮后,网格将返回到预编辑模式,但其更改已丢失。 但是,没有视觉反馈表明他们的更改没有坚持。 理想情况下,如果用户的更改因并发冲突而丢失,我们会通知他们,并可能使网格保持编辑模式。 让我们看一下如何完成此操作。

步骤 3:确定何时发生并发冲突

由于并发冲突会拒绝用户所做的更改,因此最好在发生并发冲突时向用户发出警报。 为了提醒用户,让我们将标签 Web 控件添加到名为 ConcurrencyViolationMessage 的页面顶部,其 Text 属性显示以下消息:您尝试更新或删除另一个用户同时更新的记录。 请查看其他用户的更改,然后恢复更新或删除。 将 Label 控件的 CssClass 属性设置为 Warning,这是在 中 Styles.css 定义的 CSS 类,它以红色、斜体、粗体和大字体显示文本。 最后,将 Label 和 VisibleEnableViewState 属性设置为 false。 这将隐藏 Label,但仅那些我们将其属性显式设置为 Visibletrue的回发除外。

将标签控件添加到页面以显示警告

图 8:将标签控件添加到页面以显示警告 (单击以查看全尺寸图像)

执行更新或删除时,GridView RowUpdatedRowDeleted 事件处理程序在其数据源控件执行请求的更新或删除后触发。 我们可以确定有多少行受这些事件处理程序的操作的影响。 如果零行受到影响,我们希望显示 ConcurrencyViolationMessage 标签。

RowUpdatedRowDeleted 事件创建事件处理程序,并添加以下代码:

protected void Products_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.AffectedRows == 0)
    {
        ConcurrencyViolationMessage.Visible = true;
        e.KeepInEditMode = true;
        // Rebind the data to the GridView to show the latest changes
        Products.DataBind();
    }
}
protected void Products_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
    if (e.AffectedRows == 0)
        ConcurrencyViolationMessage.Visible = true;
}

在这两个事件处理程序中,e.AffectedRows我们检查 属性,如果它等于 0,请将 ConcurrencyViolationMessage Label 属性Visible设置为 true。 在事件处理程序中 RowUpdated ,我们还通过将 属性设置为 KeepInEditMode true 来指示 GridView 保持编辑模式。 在执行此操作时,我们需要将数据重新绑定到网格,以便将其他用户的数据加载到编辑界面中。 这是通过调用 GridView 方法实现的 DataBind()

如图 9 所示,使用这两个事件处理程序时,每当发生并发冲突时,都将显示一条非常明显的消息。

出现并发冲突时显示消息

图 9:在“面对并发冲突 (单击 以查看全尺寸图像)

总结

创建多个并发用户可能编辑相同数据的 Web 应用程序时,请务必考虑并发控制选项。 默认情况下,ASP.NET 数据 Web 控件和数据源控件不使用任何并发控件。 如本教程所示,使用 SqlDataSource 实现乐观并发控制相对快速且简单。 SqlDataSource 处理向自动生成的 和 DELETE 语句添加扩充WHERE子句的大部分工作,但在处理NULL值列方面有一些细微之处,如正确处理NULL值部分所述。UPDATE

本教程结束了对 SqlDataSource 的检查。 其余教程将返回到使用 ObjectDataSource 和分层体系结构处理数据。

编程快乐!

关于作者

斯科特·米切尔是七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自 1998 年以来一直在使用 Microsoft Web 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。