使用 SqlDataSource 实现并发优化

本文档是 Visual C# 教程 (切换到 Visual Basic 教程)

在本教程中,我们将讨论并发优化基础课程,然后探讨如何使用 SqlDataSource 控件。

« 前一篇教程  |  下一篇教程 »

简介

在前一篇教程中,我们探讨了如何向 SqlDataSource 控件添加插入、更新和删除功能。简而言之,要提供这些功能,我们需要在控件的 InsertCommand 、UpdateCommand 或 DeleteCommand 属性指定相应的 INSERT 、UPDATE 、或 DELETE SQL 语句,并在InsertParameters 、UpdateParameters 和DeleteParameters 集合中指定适当的参数。这些属性和集合可以手动指定,Configure Data Source 向导的 Advanced 按钮提供了一个 “ Generate INSERT, UPDATE, and DELETE statements” 复选框,该复选框将根据 SELECT 语句自动生成这些语句。

除了“Generate INSERT, UPDATE, and DELETE statements” 复选框之外,高级SQL 生成选项还包括一个“Use optimistic concurrency” 选项(见图1 )。选中该选项后,如果在用户上一次将数据加载到网格之后没有修改基础数据库数据,则将修改UPDATE 和 DELETE 中自动生成的 WHERE 子句,只执行更新和删除操作。

图1 :可以从 Advanced SQL Generation Options 对话框添加并发优化支持

返回实施并发优化 教程,我们探讨了并发优化的基础知识,以及如何向ObjectDataSource 添加并发优化控件。在本教程中,我们将再次探讨并发优化控件的基础知识,然后探讨如何使用 SqlDataSource 实现。

并发优化简介

对于允许多个用户同时编辑或删除相同数据的Web 应用程序来说,某个用户有可能会偶然覆盖其它用户的更改。在实施并发优化 教程中,我将给出下列示例:

假设两个用户Jisun 和 Sam 均在应用程序中访问某一页面,该页面允许访问者通过GridView 控件更新和删除产品。两者在差不多相同的时间均单击了 “Chai” 的 Edit 按钮。Jisun 将产品名称更改为 “Chai Tea” ,并单击了Update 按钮。最终结果是,UPDATE 语句被发送到数据库,该语句设置了产品所有的可更新字段(尽管 Jisun 仅更新了一个字段 ProductName )。 此时,数据库中此产品包含了 “Chai Tea” ,类别 Beverages 和供应商 Exotic Liquids 等值。但是,Sam 屏幕上的 GridView 仍然在可编辑 GridView 行中显示产品名称为 “Chai” 。Jisun 完成更改之后几秒钟 ,Sam 将类别更新为 Condiments ,并单击了Update 。这导致 PDATE 语句将被发送到数据库,该数据库将产品名称设置为 “Chai” ,而CategoryID 则被设置为相应的 Condiments 类别 ID ,其它的与此类似。Jisun 的对产品名称的更改已经被覆盖。

图2 显示了此交互过程。

图2 :两个用户同时更新记录时,一个用户的更改可能覆盖另外一个用户的更改

为了防止这种情况出现,必须执行某种形式的并发控件并发优化 作为本教程的重点,将假定尽管偶尔存在并发冲突,但是绝大多数情况下这种冲突并不会出现。因此,如果发生冲突,并发优化控件将通知用户无法保存他们所做的修改,因为其它用户已经修改了同一数据。

注意:对于那些假设会存在很多并发冲突,或者这些冲突是根本不容许的应用来说,可使用保守式控件。有关保守式控件的更详细讨论,请参照实施并发优化 教程。

并发优化控件的作用是确保正在更新或删除的记录保持其更新或删除过程开始的同样的值。例如,在可编辑 GridView 中单击 Edit 按钮时,将从数据库读取记录值,并在文本框和其它 Web 控件中显示记录值。这些原始值由 GridView 保存。随后,在用户完成更改并单击 Update 按钮之后,使用的 UPDATE 语句必须考虑原始值和新值,并且如果开始编辑的原始值与数据库中的值完全相同,则仅更新基础数据库记录。图 3 描述了事件的顺序。

图3 :为使更新或删除成功,原始值必须等于当前数据库的值

有多种方式可执行并发优化(请参阅 Peter A. Bromberg并发优化更新逻辑 ,查看多种选择)。SqlDataSource 使用的技术(和数据访问层中使用的 ADO.NET 强类型 DataSets )将添加WHERE 子句,从而包含对所有原始值的比较。例如,只有当目前的数据库值等于更新 GridView 的记录时最初检索到的值时,下面的 UPDATE 语句才更新产品的名称和价格。参数 @ProductName 和 @UnitPrice 包含了由用户输入的新值,而 @original_ProductName 和 @original_UnitPrice 包含了单击 Edit 按钮时最初加载到 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 控件,放置到设计器上,将其ID 属性设置为 ProductsDataSourceWithOptimisticConcurrency 。接下来,从控件的智能标记单击 Configure Data Source 链接。在向导中的第一个屏幕上,选择使用NORTHWINDConnectionString ,并单击 Next 。

图4 :选择使用 NORTHWINDConnectionString

在本例中,我们将添加一个能够帮助用户编辑 Products 表的 GridView 。因此,在 “ Configure the Select Statement” 屏幕中,从下拉列表选择 Products列表,并选择 ProductID 、ProductName 、UnitPrice 和 Discontinued 列,如图 5 所示。

图5 :从 Products 表返回 ProductID 、ProductName 、UnitPrice 和 Discontinued 列

在选定列之后,单击Advanced 按钮,显示 Advanced SQL Generation Options 对话框。选择 “Generate INSERT, UPDATE, and DELETE statements” 和 “Use optimistic concurrency” 复选框,单击OK (屏幕快照请参照图 1) 。单击 Next ,然后再单击 Finish 完成向导。

完成 Configure Data Source 向导后,请花点时间检查结果的 DeleteCommand 和 UpdateCommand 属性,以及 DeleteParameters 和 UpdateParameters 集合。完成此操作的最简单的方式是单击左下角的 Source 选项卡,查看页面的声明式语法。在此处将会发现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 集合中有7个参数:

<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>

除了为WHERE 子句增加 UpdateCommand 和DeleteCommand 属性外(以及向各自的参数集添加其它参数外),选择“Use optimistic concurrency” 选项,调整两个其它属性 :

Web 数据 控件调用 SqlDataSource 的 Update() 或者Delete() 方法时,它将传递原始值。如果 SqlDataSource 的 ConflictDetection 属性设置为 CompareAllValues ,命令中将添加这些原始值。OldValuesParameterFormatString 属性提供了这些原始值参数使用的命名形式。Configure Data Source 向导使用 “original_{0}” ,并在UpdateCommand 和 DeleteCommand 属性以及UpdateParameters 和 DeleteParameters 集中命名相应的原始参数。

注意:由于我们未使用 SqlDataSource 控件的插入功能,您可以按照您自己的意愿删除InsertCommand 属性及其InsertParameters 集合。

正确处理 NULL 值

遗憾的是,在使用并发优化时,由 Configure Data Source 向导所自动生成的扩充的UPDATE 和 DELETE 语句无法处理包含 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

Products 表中的 UnitPrice 列不能包含 NULL 值。如果某条特定记录的UnitPrice 包含了一个 NULL 值,则 WHERE 子句部分“[UnitPrice] = @original_UnitPrice” 将始终等于 False ,这是因为NULL = NULL 始终返回 False 。因此,包含 NULL 值的记录不能进行修改或删除,因为 UPDATE 和 DELETE 语句的 WHERE 子句不会返回需要更新和删除的任何行。

注意:这个问题已于 2004 年 6 月第一次在 SqlDataSource 生成错误的 SQL 语句 中报告给微软,据传将在下一版本的ASP.NET 中解决。

要解决该问题,我们必须在 UpdateCommand 和 DeleteCommand 属性中为可拥有 NULL 值的所有列手动更新WHERE 子句。通常情况下,请将[ColumnName] = @original_ColumnName 更改为:

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

此修改可直接通过声明式标记完成,您可以从Properties 窗口使用 UpdateQuery 或DeleteQuery 选项,或者在 Configure Data Source 向导中使用 “Specify a custom SQL statement or stored procedure” 中的 UPDATE 和 DELETE 选项卡。同样,必须对可能包含 NULL 值的 UpdateCommand 和 DeleteCommand 的 WHERE 子 句中的每一列 进行修改。

将这种情况应用到我们的例子中将使得UpdateCommand 和 DeleteCommand 值修改:

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 添加 Edit 和 Delete 选项

将SqlDataSource 配置为支持并发优化之后,剩下需要做的事情是向页面中添加使用此并发控件的Web 数据 控件。在本教程中,我们将添加一个既可以支持编辑功能,又可以支持删除功能的 GridView 。要完成此操作,从工具栏中拖拽一个 GridView 到 设计器 中,并将其ID 设置为 Products 。在 GridView 的智能标记中,将其绑定到步骤 1 中添加的 ProductsDataSourceWithOptimisticConcurrency SqlDataSource 控件。最后,从智能标记选中 “Enable Editing” 和 “Enable Deleting” 选项。

图6 :将 GridView 绑定到 SqlDataSource ,并启用编辑和删除功能

在添加GridView 之后,通过删除 ProductID BoundField ,将 ProductName BoundField 的HeaderText 属性更改为 “Product” ,以及更新UnitPrice BoundField 的方式配置其外观,从而其 HeaderText 属性变为 “Price” 。理想情况下,我们应该加强编辑界面,使之包括一个ProductName 值的 RequiredFieldValidator,以及一个 UnitPrice 值的 CompareValidator (保证其数值可保持正确格式)有关定制 GridView 的编辑界面的更详细信息,请参阅自定义数据修改界面 教程。

注意:由于从 GridView 获取的原始值存储在视图状态下,因此必须启用GridView 的视图状态。

完成对 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 页。在两个浏览器中分别单击第一个产品的 Edit 按钮。在第一个浏览器中,改变产品名称,并单击 Update 。此浏览器将回传,而 GridView 将返回到其预编辑模式,显示新编辑记录的新的产品名称。

在第二个浏览器窗口中,更改价格(但是保留产品名称为原始值),并单击 Update 。回传时,网格返回其预编辑模式,但并未记录价格更改。第二个浏览器中显示的值(新产品名称和旧价格)与第一个浏览器中的完全一样。第二个浏览器窗口中所作的更改已经丢失。而且,由于没有显示任何表明出现并发异常的异常或信息,更改丢失得非常平静。

图7 :第二个浏览器中所作的更改毫无声息的丢失了

没有实现对第二个浏览器进行修改的原因在于UPDATE 语句的 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 不会影响任何记录。

注意 : 删除的工作方式 相同。打开两个浏览器窗口,首先,编辑某一特定产品,接下来,保存所作更改。在一个浏览器中保存所作更改后,在另外一个浏览器中单击同一产品的 Delete 按钮。由于DELETE 语句的 WHERE 子句中的原始值不匹配,因此删除操作毫无声息的以失败告终。

从最终用户的角度看,在第二个用户的浏览器窗口中单击 Update 按钮后,网格将返回到预编辑模式,但是它们所作的修改已经丢失。但是,此处不存在不保持更改的可见回传。理想情况下,如果由于并发冲突的原因导致用户所作更改丢失,我们应该通知他们,或者将网格保持在编辑模式下。下面,我们探讨一下如何完成此操作。

步骤3 :确定出现并发冲突的时间

由于并发冲突拒绝了我们所作的修改,因此,最好能够在出现并发冲突时提醒用户。要想提醒用户,我们需要首先在页面的顶部添加一个名为ConcurrencyViolationMessage 的Web 标签控件,其文本属性显示下列信息:" You have attempted to update or delete a record that was simultaneously updated by another user. Please review the other user's changes and then redo your update or delete."将标签控件的CssClass 属性设置为 “Warning” ,该属性为 Styles.css 中定义的CSS 类,可以红色、斜体、粗体和大字体显示文本。最后,将标签 的Visible 和 EnableViewState 属性设置为False 。除非我们在这些回传中明确将其 Visible 属性设置为 True , 否则将隐藏标签 。

图8 :在页面上添加一个显示警告的标签控件

执行更新或删除操作时,GridView 的 RowUpdated 和 RowDeleted Event Handler 将在其数据源控件完成所需的更新或删除操作后释放。我们可以确定这些 Event Handler 操作所影响的行数。如果影响为零行,我们希望显示ConcurrencyViolationMessage 标签 。

请为 RowUpdated 和 RowDeleted 事件创建 Event Handler ,并添加下列代码:

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

在两个Event Handler 中,选中 e.AffectedRows 属性,如果它等于 0 ,则将 ConcurrencyViolationMessage 标签 的Visible 属性设置为 True 。在 RowUpdated Event Handler 中,我们还可通过将 KeepInEditMode 设置为 true 来指定 GridView 保持编辑模式。通过这种方式,我们需要将数据重新绑定到网格,从而保证其它用户的数据将被加载到编辑界面。此操作可通过调用 GridView 的 DataBind() 方法完成。

如图9 所示,通过使用这两个 Event Handler ,出现并发冲突时,屏幕上将显示一条非常醒目的信息。

图9 :出现并发冲突时显示信息

小结

在多个并发用户可能编辑相同数据的情况下,创建web 应用程序时需要考虑并发控件选项就变得十分重要。在默认情况下, ASP.NET Web 数据控件和数据源控件不使用任何并发控件。正如我们在本教程中所看到的,使用 SqlDataSource 实现并发优化控件非常快捷和便利。SqlDataSource 可以为您处理大多数日常工作—— 向自动生成的 UPDATE 和 DELETE 语句添加扩充 WHERE 子句,但是,正如在 “正确处理NULL 值” 部分所讨论的,几乎不存在处理 NULL 值列的示例。

本教程是最后一篇对 SqlDataSource 进行 探讨的教程。此后的教程中探讨使用 ObjectDataSource 和分层架构处理数据。

快乐编程!

 

下一篇教程