作者 :斯科特·米切尔
在本教程中,我们回顾了乐观并发控制的基本原理,然后探索如何使用 SqlDataSource 控件实现它。
介绍
在前面的教程中,我们介绍了如何将插入、更新和删除功能添加到 SqlDataSource 控件。 简言之,为了提供这些功能,我们需要在控件的InsertCommand
、UpdateCommand
或DeleteCommand
属性中指定相应的INSERT
、UPDATE
或DELETE
SQL 语句,并在InsertParameters
、UpdateParameters
和DeleteParameters
集合中提供合适的参数。 虽然可以手动指定这些属性和集合,但“配置数据源”向导中的“高级”按钮提供了一个“生成 INSERT
、UPDATE
和 DELETE
语句”的复选框,该复选框将基于 SELECT
语句自动创建这些语句。
除了“生成INSERT
”和UPDATE
DELETE
“语句”复选框外,“高级 SQL 生成选项”对话框还包括“使用乐观并发”选项(请参阅图 1)。 选中后,自动生成的 UPDATE
和 DELETE
语句中的 WHERE
子句将被修改,以便仅在用户上次将数据加载到网格后基础数据库数据尚未被修改时执行更新或删除。
图 1:你可以从“高级 SQL 生成选项”对话框中添加乐观并发支持
回到 “实现乐观并发 ”教程,我们研究了乐观并发控制的基本原理以及如何将其添加到 ObjectDataSource。 在本教程中,我们将修改乐观并发控制的基本要素,然后探讨如何使用 SqlDataSource 实现它。
乐观并发总结
对于允许多个用户同时编辑或删除相同数据的 Web 应用程序,一个用户可能会意外覆盖其他用户的更改。 在 “实现乐观并发 ”教程中,我提供了以下示例:
假设两个用户 Jisun 和 Sam 都访问了应用程序中的页面,允许访问者通过 GridView 控件更新和删除产品。 他们几乎同时单击 Chai 的“编辑”按钮。 Jisun 将产品名称更改为柴茶,然后单击“更新”按钮。 净结果是生成一条UPDATE
语句发送到数据库,这条语句设置了所有产品的可更新字段(即使 Jisun 仅更新了一个字段ProductName
)。 此时,数据库中此特定产品的值包括柴茶、饮料类别、供应商异国液体等。 但是,Sam 屏幕上的 GridView 仍以 Chai 的形式在可编辑的 GridView 行中显示产品名称。 几秒钟后,Jisun 的更改被提交,Sam 将类别更新为调味品并点击“更新”。 这会导致 UPDATE
发送到数据库的语句,该语句将产品名称设置为 Chai、 CategoryID
对应的 Condiments 类别 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:创建支持乐观并发的 SQL数据源
首先从OptimisticConcurrency.aspx
文件夹中打开SqlDataSource
页面。 将 SqlDataSource 控件从工具箱拖到设计器上,将其 ID
属性 ProductsDataSourceWithOptimisticConcurrency
设置为 。 接下来,单击控件智能标记中的“配置数据源”链接。 在向导的第一个屏幕中,选择使用 NORTHWINDConnectionString
并单击“下一步”。
图 4:选择“使用 NORTHWINDConnectionString
”(单击以查看全尺寸图像)
在本示例中,我们将添加一个 GridView,使用户能够编辑 Products
表。 因此,在“配置 Select 语句”屏幕中,从下拉列表中选择 Products
表,然后选择 ProductID
、ProductName
、UnitPrice
和 Discontinued
列,如图 5 所示。
图 5:从Products
表格中返回ProductID
、ProductName
、UnitPrice
和Discontinued
列(单击以查看全尺寸图像)
选取列后,单击“高级”按钮以显示“高级 SQL 生成选项”对话框。 选中“生成 INSERT
”、“UPDATE
”、DELETE
,以及“使用乐观并发”复选框,然后单击“确定”(请参阅图 1 以获取屏幕截图)。 完成向导的步骤是先单击“下一步”,然后单击“完成”。
完成“配置数据源”向导后,花点时间检查生成的DeleteCommand
属性和UpdateCommand
属性以及DeleteParameters
UpdateParameters
集合。 执行此作的最简单方法是单击左下角的“源”选项卡以查看页面的声明性语法。 你将在此处找到一个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>
除了扩充WHERE
的UpdateCommand
子句和DeleteCommand
属性(并将其他参数添加到相应的参数集合)之外,选择“使用乐观并发”选项还会调整另外两个属性。
- 将
ConflictDetection
属性 从OverwriteChanges
(默认值) 更改为CompareAllValues
- 将
OldValuesParameterFormatString
属性 从 {0} (默认值) 更改为 original_{0} 。
当数据 Web 控件调用 SqlDataSource 或Update()
Delete()
方法时,它会传入原始值。 如果 SqlDataSource 属性 ConflictDetection
设置为 CompareAllValues
,这些原始值将添加到命令中。 该 OldValuesParameterFormatString
属性提供用于这些原始值参数的命名模式。 “配置数据源”向导使用original_{0},并在UpdateCommand
和DeleteCommand
属性以及UpdateParameters
和DeleteParameters
集合中分别命名每个原始参数。
注释
由于我们不使用 SqlDataSource 控件的插入功能,因此可以随意删除 InsertCommand
属性及其 InsertParameters
集合。
正确处理NULL
值
遗憾的是,通过“配置数据源”向导使用乐观并发时自动生成的UPDATE
和DELETE
扩充语句不适用于包含NULL
值的记录。 要了解原因,请考虑我们的 SqlDataSource s 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
值。 如果特定记录的NULL
为UnitPrice
,则WHERE
子句中的[UnitPrice] = @original_UnitPrice
将始终被判定为 False,因为NULL = NULL
始终返回 False。 因此,不能编辑或删除包含 NULL
值的记录,因为 UPDATE
and DELETE
语句 WHERE
子句不会返回任何行来更新或删除。
注释
此 bug 于 2004 年 6 月在 SqlDataSource 中首次报告给 Microsoft ,生成不正确的 SQL 语句 ,据报道,该 bug 计划在下一版本的 ASP.NET 中修复。
若要解决此问题,我们必须手动更新UpdateCommand
和DeleteCommand
属性中WHERE
子句,以便适用于所有可能具有NULL
值的列。 一般情况下,更改为 [ColumnName] = @original_ColumnName
:
(
([ColumnName] IS NULL AND @original_ColumnName IS NULL)
OR
([ColumnName] = @original_ColumnName)
)
可以通过声明性标记、“属性”窗口中的 UpdateQuery 或 DeleteQuery 选项,或通过“配置数据源”向导中的“指定自定义 SQL 语句或存储过程”选项中的 UPDATE 和 DELETE 选项卡直接进行此修改。 同样,必须对在 UpdateCommand
和 DeleteCommand
s WHERE
子句中可能包含 NULL
值的每个列进行此修改。
将此应用到我们的示例会导致以下修改 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
配置 SqlDataSource 以支持乐观并发后,只需在页面中添加一个利用这种并发控制的数据 Web 控件。 在本教程中,让我们添加一个 GridView,该网格视图提供编辑和删除功能。 为此,请将 GridView 从工具箱拖到设计器上,并将其设置为 ID
Products
。 从 GridView 智能标记中,将其绑定到步骤 1 中添加的 ProductsDataSourceWithOptimisticConcurrency
SqlDataSource 控件。 最后,检查智能标记中的“启用编辑”和“启用删除”选项。
图 6:将 GridView 绑定到 SqlDataSource 并启用编辑和删除(单击可查看全尺寸图像)
添加 GridView 后,配置其外观:通过删除 ProductID
BoundField、将 ProductName
BoundField 的 HeaderText
属性更改为 Product,并更新 UnitPrice
BoundField,使得其 HeaderText
属性仅为 Price。 理想情况下,我们应该增强编辑界面,增加ProductName
值的RequiredFieldValidator和UnitPrice
值的CompareValidator(确保它是格式正确的数值)。 若要更深入地了解如何自定义 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
语句中的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
语句中的WHERE
子句,因此删除会以无提示方式失败。
从第二个浏览器窗口中的最终用户的角度来看,单击“更新”按钮后,网格返回到预编辑模式,但更改已丢失。 但是,没有视觉反馈,他们的更改没有坚持。 理想情况下,如果用户的更改由于并发冲突而丢失,我们会通知他们,并且可能让网格保持在编辑模式。 让我们看看如何实现此目的。
步骤 3:确定发生并发冲突的时间
由于并发冲突会拒绝已做出的更改,因此在发生并发冲突时提醒用户会很好。 若要提醒用户,请将标签 Web 控件添加到其ConcurrencyViolationMessage
Text
属性显示以下消息的页面顶部:您尝试更新或删除其他用户同时更新的记录。 请查看其他用户的更改,然后重做更新或删除。 将 Label 控件的属性 CssClass
设置为 Warning,这是一个 CSS 类,该类以 Styles.css
红色、斜体、粗体和大字体显示文本。 最后,将标签 Label 的 Visible
和 EnableViewState
属性设置为 False
。 这将隐藏标签控件,除非在回发中我们显式将其属性Visible
设置为True
。
图 8:向页面添加标签控件以显示警告(单击以查看全尺寸图像)
执行更新或删除时,GridView s RowUpdated
和 RowDeleted
事件处理程序会在其数据源控件执行请求的更新或删除后触发。 我们可以从这些事件处理程序中确定操作影响了多少行。 如果零行受到影响,我们希望显示 ConcurrencyViolationMessage
标签。
为 RowUpdated
事件和 RowDeleted
事件创建事件处理程序并添加以下代码:
Protected Sub Products_RowUpdated(sender As Object, e As GridViewUpdatedEventArgs) _
Handles Products.RowUpdated
If e.AffectedRows = 0 Then
ConcurrencyViolationMessage.Visible = True
e.KeepInEditMode = True
' Rebind the data to the GridView to show the latest changes
Products.DataBind()
End If
End Sub
Protected Sub Products_RowDeleted(sender As Object, e As GridViewDeletedEventArgs) _
Handles Products.RowDeleted
If e.AffectedRows = 0 Then
ConcurrencyViolationMessage.Visible = True
End If
End Sub
在这两个事件处理程序中,我们检查属性 e.AffectedRows
,如果它等于 0,请将 ConcurrencyViolationMessage
Label 属性 Visible
设置为 True
。 在事件处理程序中 RowUpdated
,我们还指示 GridView 通过将属性设置为 KeepInEditMode
true 来保持编辑模式。 为此,我们需要将数据重新绑定到网格,以便其他用户的数据加载到编辑界面中。 这是通过调用 GridView 的 DataBind()
方法实现的。
如图 9 所示,使用这两个事件处理程序时,每当发生并发冲突时,会显示一条非常明显的消息。
图 9:出现并发冲突时显示一条消息(单击以查看全尺寸图像)
概要
创建多个并发用户可能编辑相同数据的 Web 应用程序时,请务必考虑并发控制选项。 默认情况下,ASP.NET 数据 Web 控件和数据源控件不使用任何并发控制。 如本教程中所示,使用 SqlDataSource 实现乐观并发控制相对快速且简单。 SqlDataSource 为你处理将扩充 WHERE
子句添加到自动生成的 UPDATE
和 DELETE
语句中的大部分繁重工作,但在处理 NULL
值列时需要注意一些细节,详见“正确处理 NULL
值”部分。
本教程总结了对 SqlDataSource 的检查。 剩余教程将返回使用 ObjectDataSource 和分层体系结构处理数据。
快乐编程!
关于作者
斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 山姆教你在24小时内掌握 ASP.NET 2.0。 可以通过 mitchell@4GuysFromRolla.com 联系到他。