ASP.NET 2.0对SqlDataSource控件使用开放式并发

 在“高级SQL生成选项”对话框里有个“使用开放式并发”选项(见图1)。当

  选择该项后,数据库中的数据在自上一次成功保存以来没发生任何改变的情况下,才能成功地执行更新或删除操作。

ASP.NET 2.0对SqlDataSource控件使用开放式并发_第1张图片

  图1:在“高级SQL生成选项”对话框添加开放式并发支持

  在Implementing Optimistic Concurrency教程路我们探讨了开放式并发控制的基本原理以及如何对ObjectDataSource控件使用开放式并发。在本教程我们看如何对SqlDataSource控件使用开放式并发。

  新的开放式并发

  在一个允许多人同时编辑或删除相同数据的应用程序里,有这种可能:一个人修改后的记录意外地被另一个人修改的记录所覆盖。在Implementing Optimistic Concurrency 这篇教程我们例举过这样的例子:

  例如,假设两个用户,Jisun和Sam,都访问我们的应用软件中的一个页面,这个页面允许访问者通过一个GridView控件更新和删除产品数据。他们都同时点击GridView控件中的Edit按钮。Jisun把产品名称更改为“Chai Tea”并点击Update按钮,实质结果是向数据库发送一个UPDATE语句,它将更新此产品的所有可修改的字段(尽管Jisun实际上只修改了一个字段:ProductName)。在这一刻,数据库中包含有这条产品记录“Chai Tea”—种类为Beverages、供应商为Exotic Liquids、等该产品的详细信息。然而,在Sam的屏幕中的GridView里,当前编辑行里显示的产片名称依旧是“Chai”。在Jisun的更改被提交后片刻,Sam把种类更改为“Condiments”并点击Update按钮。这个发送到数据库的UPDATE语句的结果是将产品名称更改为“Chai”、CategoryID字段的值是种类Beverages对应的ID,等等。Jisun所作的对产品名称的更改就被覆盖了。
 图2展示了这些连续的事件

 图2:当两个用户同时更新一条记录,则存在一个用户的更改覆盖另一个的更改的可能性

  为了应对这种可能性,我们必须执行某种并发控制。本文的焦点——开放式并发控制便是其中之一,它适合于这种情况:假定并发冲突只是偶尔发生,绝大多数的时候并不会出现。当发生一个冲突时,仅仅简单的告知用户,他所作的更改不能保存,因为别的用户已经修改了同一条记录。

  注意:对应用程序来说,假定并发冲突经常发生,且无法容忍。在这种情况下最后用保守式并发控制。关于保守式并发控制的更多讨论,请参考

  Implementing Optimistic Concurrency 教程。

  开放式并发控制的作用在于:确保要更新或删除的记录的值与该记录在updating or deleting阶段的值相同。比如,例如,当在一个可编辑的GridView里点击编辑按钮时,该记录的原始值从数据库中读取出来并显示在TextBox和其他Web控件中。这些原始的值保存在GridView里。随后,当用户完成他的修改并点击更新按钮,这些原始值加上修改后的新值发送到业务逻辑层,然后到数据访问层。数据访问层必定发出一个SQL语句,它将仅仅更新那些开始编辑时的原始值根数据库中的值一致的记录。图3描述了这些事件发生的顺序。

 图3:为了更新或删除能够成功,原始值必须与数据库中相应的值一致

 有多种方法可以实现开放式并发控制(查看Peter A. Bromberg的文章 Optmistic Concurrency Updating Logic,从摘要中看到许多选择)。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能实现开放式并发控制是很简单的事情。

  第一步:创建一个支持开放式并发的SqlDataSource控件

  打开SqlDataSource文件夹中的OptimisticConcurrency.aspx页面,从工具箱拖一个SqlDataSource控件到页面,设置其ID为ProductsDataSourceWithOptimisticConcurrency。在其智能标签里点“设置数据源”,数据库选为“NORTHWINDConnectionString”,点下一步。

ASP.NET 2.0对SqlDataSource控件使用开放式并发_第2张图片

图4:选“ORTHWINDConnectionString”数据库

  在此例子里,我们将添加一个GridView控件以编辑表Products。所以在“Configure the Select Statement”界面选择从表Products返回ProductID, ProductName, UnitPrice和Discontinued列,如图5所示:

 

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

  然后,点“高级”按钮,打开“Advanced SQL Generation Options”对话框,选择“Generate INSERT, UPDATE, and DELETE statements”和“Use optimistic concurrency”2项,点“OK”(见图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>标签里有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>

---------------------------------------------------------------------

选择了“Use optimistic concurrency”选项后,不仅扩展了UpdateCommand 和DeleteCommand属性里的WHERE字句(同时在相关参数集里添加了参数),同时调整了以下2个属性:

  1. 将ConflictDetection属性由“OverwriteChanges”(默认值)改为  “CompareAllValues ”

  2. 将OldValuesParameterFormatString属性由“{0}”(默认值)改为  “original_{0}”

 

当数据Web控件调用SqlDataSource的Update()或Delete()方法时,它将传递原始值。当SqlDataSource的ConflictDetection属性设置为“CompareAllValues”时,就会将这些原始值添加到命令中。而OldValuesParameterFormatString属性则为这些原始值提供了命名规范,向导以“original_{0}”的形式为 UpdateCommand和DeleteCommand中的原始值以及<UpdateParameters>和<DeleteParameters>中的参数命名。

  注意:

  由于我们没有使用SqlDataSource控件的插入功能,因此可以将InsertCommand 属性和<InsertParameters>标签清除。

  正确地处理NULL值

  不幸的是,当使用开放式并发的时候,由设置数据源向导自动生成的、扩展成包含WHERE字句的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。所以凡是y包含NULL值的记录不能被编辑或删除,因为UPDATE和DELETE命令中的WHERE字句不能返回记录。 

注意:

  这个漏洞最早于2004年6月报告给微软,据业内传言,微软将在ASP.NET的下一个版本修补该漏洞。

  为修补该漏洞,我们需要在UpdateCommand和DeleteCommand属性里手工修改所有允许为NULL值的列。一般来说,将[ColumnName] = @original_ColumnName to改成:

-----------------------------

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

-----------------------------

你可以在属性窗口的UpdateQuery或DeleteQuery选项的代码声明里修改,或者在设置数据源向导的“指定自定义SQL语句或存储过程”选项的“更新”和“删除”标签里修改。确保在UpdateCommand和DeleteCommand的WHERE字句里做相同的修改。如下:

---------------------------------------

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,并绑定到第一步添加的SqlDataSource控件ProductsDataSourceWithOptimisticConcurrency,最后启用其“编辑”和“删除”功能。

ASP.NET 2.0对SqlDataSource控件使用开放式并发_第3张图片

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

  添加GridView控件后,优化其界面。将ProductID列移除;将ProductName列的HeaderText属性设置为“Product”;同样,UnitPrice列的设置为“Price”。另外,我们最好为ProductName添加一个RequiredFieldValidator控件;为UnitPrice添加一个CompareValidator控件(确保其为格式化的数字值)。参考教程Customizing the Data Modification Interface看如何自定义GridView界面。

  注意:

  必须确保激活GridView控件的view state(视图状态),因为GridView控件传递原始值时,将原始值保存在view state中。

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

----------------------------------

 

来实际地感受一下开放式并发控制。在2个浏览器里同时打开OptimisticConcurrency.aspx页面,且都点击第一条记录的编辑按钮。在第一个浏览器里改变产品名称并点“编辑”。浏览器将发生回传,GridView控件又回到“预编辑”状态,显示新的产品名称。

  在第2个浏览器里,改变产品的价格(不要改产品名称)后,点“编辑”。发生回传,GridView控件又回到“预编辑”状态,和第1个浏览器显示的结果一样

  ——产品的名称改变了但价格没改变,第2个浏览器做的修改失败了。然而,一切都发生的那么静悄悄,没有任何提示刚才发生了并发冲突!

 

ASP.NET 2.0对SqlDataSource控件使用开放式并发_第4张图片

 图7:第2个浏览器所做的修改悄悄的丢失了

  第2个浏览器更新失败的原因在于: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

----------------------------------

当第2个浏览器更新记录时,WHERE字句里的原始产品名(即Chai)与当前任意一条记录的产品名不匹配(因为第1个浏览器将Chai改为了Chai Tea)。所以表达式“[ProductName] = @original_ProductName ”返回False,导致更新失败。

注意:

  删除的原理于此相同。同时打开2个浏览器,第1个先对某个 产品 作更改,再在第2个浏览器删除该产品,同样是因为原始值与更新后的值不匹配,删除失败。

  在最终用户(更新失败的那个)看来,他点“更新”按钮后,GridView控件返回“预编辑”状态,但提交的修改丢失了。然而没有任何直观的提醒表明修改失败。当用户的更新因并发冲突失败时,我们最好提醒用户,比如将GridView控件

  保持在“编辑”状态。下面我们来看如何实现这一点。

  第3步:并发冲突的处理

  因为并发冲突拒绝用户的更改,所以当发生并发冲突时最好提示用户。在页面上添加一个Label控件,其ID为ConcurrencyViolationMessage,设置其Text 属性为“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中。最后,把Visible和EnableViewState属性设置为“false” 。这样Label控件将不可见,除非发生了某些回传事件(我们在这些回传事件里指定Label控件的Visible属性为true)

ASP.NET 2.0对SqlDataSource控件使用开放式并发_第5张图片

 图8:在页面添加一个Label控件用以显示提醒 信息

  执行更新或删除操作时,当GridView的数据源控件完成更新或删除后,才开始

  执行GridView控件的RowUpdated和RowDeleted事件处理器(event handler)。我们可以在这些事件处理器里计算影响了多少条记录。假如影响了0条记录,亦即操作失败,我们希望将Label控件ConcurrencyViolationMessage显示出来。

为RowUpdated和RowDeleted事件创建处理器,添加如下代码:

-------------------------------

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

-------------------------------

在这2个事件处理器中我们都要检验e.AffectedRows 属性,如果为0,设置Label控件ConcurrencyViolationMessage的Visible属性为true。特别的,在RowUpdated事件处理器中,我们通过将GridView控件的KeepInEditMode属性设置为true,使其保持在编辑状态。这样的话,通过GridView的DataBind() 方法,将他人已经成功更新的数据显示在编辑状态。

  如图9所示,当发生并发冲突时,显示提示信息

图9:当发生并发冲突时,显示提示信息:

  总结:

  创建一个应用程序时,当多人同时编辑相同数据的时候,要考虑到并发冲突的问题。在默认情况下,ASP.NET数据Web控件和数据源控件没有采取并发控制。就像我们在本章看到的一样,对SqlDataSource控件使用开放式并发控制还是比较迅速和容易的。通过在UPDATE和DELETE语句里扩展WHERE字句,SqlDataSource能应对绝大部分情况,但就像在“正确处理NULL值”部分探讨的那样,对包含NULL值列的处理有漏洞。

 

  

  作者简介

  Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。Scott是个独立的技术咨询顾问,培训师,作家。

你可能感兴趣的:(ASP.NET 2.0对SqlDataSource控件使用开放式并发)