简介
就计算机科学而言 , 缓存 过程包括成本昂贵的数据或信息的获取 , 以及将备份存储在可快速访问的位置。对于数据驱动的应用程序,大型、复杂的查询通常会消耗大量应用程序执行时间。要提升这类应用程序的性能,通常的做法是,将昂贵的数据库查询的结果存储在应用程序的内存中。
ASP.NET 2.0 提供了许多缓存方式。整个网页或用户控件所呈现的标记可通过输出缓存 进行缓存。同样, ObjectDataSource 和 SqlDataSource 控件也提供了缓存功能,允许在控件级对数据进行缓存。而 ASP.NET 的数据缓存 提供了丰富的缓存 API ,允许页面开发人员通过编程缓存对象。在本教程和接下来的三个教程中,我们将研究如何使用 ObjectDataSource 的缓存功能和数据缓存。我们也将探索如何在启动时缓存应用级数据,以及如何通过使用 SQL 缓存依赖项使缓存的数据保持最新状态。这些教程中没有涉及输出缓存。有关输出缓存的详细信息,参见 ASP.NET 2.0 中的输出缓存 。
缓存可以应用在架构中的任意位置 , 从数据访问层直至表示层。在本教程中,我们将探讨如何通过 ObjectDataSource 控件将缓存应用到表示层。在下一篇教程中,我们将研究如何在业务逻辑层缓存数据。
主要缓存概念
通过获取成本昂贵的数据并将备份存储在便于访问的位置 , 缓存可以极大地改善应用程序的整体性能和可伸缩性。由于缓存仅保留实际基础数据的备份,如果基础数据发生改变,缓存中的数据可能会 过期。为解决此问题,页面开发人员可以指定适当的标准,将缓存项从缓存中 清除。可以使用的标准包括:
- 基于时间的标准 – 可以向缓存中添加项目 , 项目的驻留时间固定或可变。例如,页面开发人员可以指定一个 60 秒的时间段。当时间段固定不变时,在将项目添加到缓存后,无论访问该项目的频率有多高,缓存项都会在 60 秒后清除。当时间段灵活可变时,缓存项会在最后一次访问超过 60 秒后清除。
- 基于依赖项的标准 – 在向缓存中添加项目时 , 可以将项目与依赖项结合。当项目的依赖项发生改变时,该项目将从缓存中清除。依赖项可以是一个文件、另一个缓存项,或这两者的混合体。ASP.NET 2.0 也启用了 SQL 缓存依赖项,允许开发人员向缓存中添加项目,并在基础数据库数据发生改变时将其清除。我们将在后面的使用 SQL 缓存依赖项教程中讨论 SQL 缓存依赖项。
无论指定哪种清除标准 , 在基于时间的标准或基于依赖项的标准得到满足之前 , 缓存中的项目也可能被清除。如果容量达到极限,缓存会在添加新项目之前删除现有项目。因此,当通过编程处理缓存的数据时,很重要的一点是,应假设缓存的数据可能不存在。在下一篇教程在架构中缓存数据 中,我们将研究从缓存中访问数据的具体模式。
缓存是提升应用程序性能的较为经济的方式。就像 Steven Smith 在他的文章 ASP.NET Caching: Techniques and Best Practices中阐述的一样:
“缓存是获得 ‘上佳’ 性能的一种好方法,不需要太多的时间和分析。……内存很便宜,因此,如果通过缓存 30 秒钟的输出(而非花费一天或一周的时间优化代码或数据库)就可以获得所需的性能,那么请采用缓存解决方案(假设 30 秒的旧数据是可接受的)然后继续。…… 最后 , 糟糕的设计总会显现出来 , 因此 , 开发人员当然应该正确地设计应用程序。但如果你只需在今天获得足够的性能,缓存是一种出色的方式。重构应用程序可以在稍后时间充足的情况下进行。”
尽管缓存可以提供显著的性能提升 , 它并不适合所有情况 , 如使用实时、频繁更新数据的应用程序 , 或不接受过期数据 ( 甚至短期过期数据 ) 的应用程序。但对于大部分应用程序,缓存都是适用的。有关 ASP.NET 2.0 中缓存技术的更多背景,参见ASP.NET 2.0 入门教程 的 缓存性能 部分。
步骤1 :创建缓存网页
在开始研究 ObjectDataSource 的缓存功能之前 , 我们会先花一些时间在我们的网站项目中创建ASP.NET 页面。这些页面需要在本教程和后面三个教程中使用。首先,添加名为 Caching 的新文件夹。接下来,将以下 ASP.NET 页面添加到该文件夹,确保每个页面与 Site.master 主页面相关联:
- Default.aspx
- ObjectDataSource.aspx
- FromTheArchitecture.aspx
- AtApplicationStartup.aspx
- SqlCacheDependencies.aspx
图1 :为缓存相关教程添加 ASP.NET 页面
Caching 文件夹中的Default.aspx 与其他文件夹中的相同 , 用于列出该部分的教程。回想一下, SectionLevelTutorialListing.ascx 用户控件提供本功能。因此,您可以从 Solution Explorer 中将此 User 控件拖拽到页面设计视图进行添加。
图2 :向 Default.aspx 添加 SectionLevelTutorialListing.ascx 用户控件
最后 , 将这些页面作为条目添加到 Web.sitemap 文件中。具体地说,就是在 “Working with Binary Data” <siteMapNode> 的后面添加以下标记:
<siteMapNode title="Caching" url="~/Caching/Default.aspx"
description="Learn how to use the caching features of ASP.NET 2.0.">
<siteMapNode url="~/Caching/ObjectDataSource.aspx"
title="ObjectDataSource Caching"
description="Explore how to cache data directly from the
ObjectDataSource control." />
<siteMapNode url="~/Caching/FromTheArchitecture.aspx"
title="Caching in the Architecture"
description="See how to cache data from within the
architecture." />
<siteMapNode url="~/Caching/AtApplicationStartup.aspx"
title="Caching Data at Application Startup"
description="Learn how to cache expensive or infrequently-changing
queries at the start of the application." />
<siteMapNode url="~/Caching/SqlCacheDependencies.aspx"
title="Using SQL Cache Dependencies"
description="Examine how to have data automatically expire from the
cache when its underlying database data is modified." />
</siteMapNode>
更新 Web.sitemap 后 , 花点时间用浏览器查看一下教程网站。现在,左侧的菜单包含了缓存教程的条目。<0}
图3 :现在 , 网站映射中包含缓存教程的条目
步骤2 :在网页中显示产品列表
本教程将研究如何使用 ObjectDataSource 控件的内置缓存功能。但在考虑这些功能之前,我们首先需要一个可进行操作的页面。让我们创建一个网页,该网页使用 GridView 列出由 ObjectDataSource 从 ProductsBLL 类获得的产品信息。
首先,在 Caching 文件夹中打开ObjectDataSource.aspx 页面。从 Toolbox 中将 GridView 拖放到 Designer , 将它的 ID 属性设置为 Products , 并从它的智能标记中选择将其绑定到名为ProductsDataSource 的新ObjectDataSource 控件。设置 ObjectDataSource 使用 ProductsBLL 类。
<0}
图4 :设置 ObjectDataSource 使用 ProductsBLL 类
在本页面 , 我们要创建一个可编辑的 GridView 。当 ObjectDataSource 中缓存的数据发生变化时,我们可以通过 GridView 的界面看到发生了什么。保留 SELECT 选项卡中下拉列表的默认值, GetProducts() ,但将 UPDATE 选项卡中的所选项目更改为接受 productName 、 unitPrice 和 productIDtab 作为输入参数的 UpdateProduct 重载方法。
图5 :将 UPDATE 选项卡的下拉列表设为适当的 UpdateProduct 重载方法
最后,将 INSERT 和 DELETE 选项卡中的下拉列表设置为 “(None)” , 然后单击 Finish 。一旦完成 Configure Data Source 向导, Visual Studio 会将 ObjectDataSource 的 OldValuesParameterFormatString 属性设置为 original_{0} 。就像在前面的教程插入、更新和删除数据概述 中探讨的那样,该属性需从声明性语法中删除,或设置回默认值 {0} ,这样我们的更新工作流在运行中才不会出错。
此外,在此向导完成时,Visual Studio 会为每个产品数据字段 在GridView 中 添加一个字段。将除 ProductName 、 CategoryName 和 UnitPrice 之外的所有 BoundField 删除。然后,将上述 3 个 BoundField 的 HeaderText 属性分别更新为 “Product” 、 “Category” 和 “Price” 。由于 ProductName 字段是必须的,将 BoundField 转换为 TemplateField 并向 EditItemTemplate 中添加一个 RequiredFieldValidator 。同样,将 UnitPrice BoundField 转换为 TemplateField 并添加一个 CompareValidator ,来确保用户输入的值是大于或等于 0 的有效货币值。在这些更改之外,开发人员还可以对外观进行任意修改,如,使 UnitPrice 值向右对齐,或指定 UnitPrice 文本在只读和编辑界面的格式。
在 GridView 的智能标记中选中 “Enable Editing” 复选框 , 允许对 GridView 进行编辑。同时,选中 “Enable Paging” 和 “Enable Sorting” 复选框。<0}
注意 :如果需要回顾如何定制 GridView 的编辑界面 ,请参考前面的定制数据修改界面 教程。
图6 :允许 GridView 支持编辑、排序和分页
在进行了这些 GridView 修改后 ,GridView 和 ObjectDataSource 的声明标记应如下所示 :
<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsDataSource"
AllowPaging="True" AllowSorting="True">
<Columns>
<asp:CommandField ShowEditButton="True" />
<asp:TemplateField HeaderText="Product" SortExpression="ProductName">
<EditItemTemplate>
<asp:TextBox ID="ProductName" runat="server"
Text='<%# Bind("ProductName") %>'></asp:TextBox>
<asp:RequiredFieldValidator
ID="RequiredFieldValidator1" Display="Dynamic"
ControlToValidate="ProductName" SetFocusOnError="True"
ErrorMessage="You must provide a name for the product."
runat="server">*</asp:RequiredFieldValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label2" runat="server"
Text='<%# Bind("ProductName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="CategoryName" HeaderText="Category"
ReadOnly="True" SortExpression="CategoryName" />
<asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
<EditItemTemplate>
$<asp:TextBox ID="UnitPrice" runat="server" Columns="8"
Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox>
<asp:CompareValidator ID="CompareValidator1"
ControlToValidate="UnitPrice" Display="Dynamic"
ErrorMessage="You must enter a valid currency value with no
currency symbols. Also, the value must be greater than
or equal to zero."
Operator="GreaterThanEqual" SetFocusOnError="True"
Type="Currency" runat="server"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemStyle HorizontalAlign="Right" />
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Bind("UnitPrice", "{0:c}") %>' />
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ProductsDataSource" runat="server"
OldValuesParameterFormatString="{0}" SelectMethod="GetProducts"
TypeName="ProductsBLL" UpdateMethod="UpdateProduct">
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
如图 7 所示,可编辑的 GridView 列出了数据库中每种产品的名称、类别和价格。花几分钟的时间对该页面的功能进行测试 – 对结果进行排序、翻页、编辑一条记录。
图7 :每个产品的名称、类别和价格显示在可排序、分页和可编辑的GridView 中
步骤3 :检查ObjectDataSource 何时在请求数据
ID 为 Products 的 GridView 通过调用 ProductsDataSource ObjectDataSource 的 Select 方法来 检索 数据 , 并将数据显示出来。ObjectDataSource 为业务逻辑层的 ProductsBLL 类创建实例并调用该类的 GetProducts() 方法,该方法又会调用数据访问层的 ProductsTableAdapter 的 GetProducts() 方法。数据访问层的方法连接到 Northwind 数据库并发出已配置好的 SELECT 查询。然后,查询数据返回到数据访问层,由数据访问层将其打包到 NorthwindDataTable 中。DataTable 对象依次返回给业务逻辑层、 ObjectDataSource 、 GridView 。之后, GridView 为 DataTable 中的每个 DataRow 创建一个 GridViewRow 对象,每个 GridViewRow 最终呈现为 HTML ,返回到客户端并显示在访问者的浏览器中。
每次 GridView 需要绑定到基础数据 , 这些事件就会按上述顺序发生。例如:首次访问页面;将数据从一个页面传递到另一个页面;为 GridView 排序;以及通过 GridView 内置的编辑或删除界面来修改 GridView 的数据。如果禁用了 GridView 的视图状态,在每次页面回传时也会重新绑定 GridView 。我们也可以通过调用 DataBind() 方法明确地将 GridView 重新绑定数据。
为了更清楚地揭示从数据库中检索数据的频率,让我们来显示一条消息,指示重新检索数据的时间。在 GridView 上添加一个名为 ODSEvents 的 Label Web 控件。清空其 Text 属性,并将 EnableViewState 属性设为 False 。在 Label 控件下方再添加一个 Button Web 控件,将其 Text 属性设为 “Postback” 。
图8 :在 GridView 上向页面添加 Label 和 Button 控件
在数据访问工作流中 , 首先触发 ObjectDataSource 的 Selecting 事件 , 然后创建基础对象并调用对应的配置好的方法。为上述事件创建一个 event handler ,并添加以下代码:
protected void ProductsDataSource_Selecting(object sender,
ObjectDataSourceSelectingEventArgs e)
{
ODSEvents.Text = "-- Selecting event fired";
}
每次 ObjectDataSource 向架构请求数据 ,Label 控件都会显示文本 “Selecting event fired” 。
在浏览器中访问该页面。首次访问该页时 , 页面上显示文本 “Selecting event fired” 。单击 “Postback” 按钮时,我们注意到文本消失了(假设 GridView 的 EnableViewState 属性设置为默认值 True )。这是因为 , 在页面回传时 ,GridView 从视图状态进行重建 , 因此不必通过 ObjectDataSource 获取数据。然而,数据的排序、分页或编辑都会使 GridView 重新绑定到数据源,因此 “Selecting event fired” 文本会重新出现。
图9 :GridView 重新绑定到数据源时,显示 “Selecting event fired”
图10 :单击 Postback 按钮导致 GridView 从视图状态进行重建
每次进行数据分页或排序都要重新检索数据库数据 , 这看起来有些浪费资源。由于我们在使用默认分页,在显示第一个页面时, ObjectDataSource 已经检索了所有记录。即使 GridView 不支持排序和分页,任何用户首次访问页面时, GridView 都必须从数据库检索数据(如果禁用了视图状态,那么每次页面回传时也会检索数据)。但如果 GridView 对所有用户显示相同的数据,那么这些额外的数据库请求都是多余的。这时,我们可以缓存 GetProducts() 方法返回的结果,并将 GridView 与这些缓存的结果绑定。
步骤4 :使用 ObjectDataSource 缓存数据
简单地设置几个属性,我们就可以配置 ObjectDataSource ,使其自动将检索数据缓存到 ASP.NET 数据缓存中。以下列表总结了与缓存相关的 ObjectDataSource 属性:
- EnableCaching– 要启用缓存必须设置为 True 。默认值为 False 。
- CacheDuration – 数据缓存的时间,以秒为单位。默认值为 0 。只有当 EnableCaching 设置为 True 且 CacheDuration 设置为大于 0 的值时, ObjectDataSource 才会缓存数据。
- CacheExpirationPolicy– 可设置为 Absolute 或 Sliding 。如果设置为 Absolute ,ObjectDataSource 缓存检索数据的时间为 CacheDuration 的值 ; 如果设置为 Sliding , 仅当未访问数据的时间达到 CacheDuration 的值时 , 数据才会过期。默认值为 Absolute 。
- CacheKeyDependency – 使用该属性将 ObjectDataSource 的缓存条目与当前缓存依赖项关联起来。通过使相关的 CacheKeyDependency 过期,我们可以提前将 ObjectDataSource 的数据条目从缓存中清除。该属性经常用于使 SQL 缓存依赖项与 ObjectDataSource 的缓存关联,我们会在后面的使用SQL 缓存依赖项教程中讨论这个主题。
让我们对 ProductsDataSource ObjectDataSource 进行配置 , 使其缓存数据的时间段为固定的 30 秒。将 ObjectDataSource 的 EnableCaching 属性设置为 True , 将 CacheDuration 属性设置为 30 。保留 CacheExpirationPolicy 属性的默认值 Absolute 。
图11 :将 ObjectDataSource 的数据缓存时间设置为 30 秒
保存更改,重新在浏览器中访问该页面。首次访问页面时,页面显示 “Selecting event fired” 文本,因为最初还未进行数据缓存。但单击 “Postback” 按钮、排序、分页或单击 Edit 或 Cancel 按钮触发的页面回传不会 再显示 “Selecting event fired” 文本。这是因为,只有当 ObjectDataSource 从基础对象获取数据时, Selecting 事件才会触发;从数据缓存中获取数据不会触发 Selecting 事件。
30 秒之后 , 数据将从缓存中清除。如果调用了 ObjectDataSource 的 Insert 、Update 或 Delete 方法,数据也会从缓存中清除。因此,以下情况都会导致 ObjectDataSource 从基础对象获取数据,并在触发 Selecting 事件时显示 “Selecting event fired” 文本:超过 30 秒、单击 Update 按钮、排序、分页或单击 Edit 或 Cancel 按钮。然后,系统再将这些返回的结果存储在数据缓存中。
注意 :如果 “Selecting event fired” 文本频繁出现,甚至在我们认为 ObjectDataSource 应使用缓存数据的时候也是如此,很可能是因为内存限制。如果内存不足,由 ObjectDataSource 添加到缓存的数据可能已被清除。如果 ObjectDataSource 没有进行正常的数据缓存,或只是偶尔进行数据缓存,请关闭一些应用程序来释放内存,然后再试一次。
图 12 阐明了ObjectDataSource 的缓存工作流。当屏幕上出现 “Selecting event fired” 文本时,原因是数据未在缓存中,且必须从基础对象中检索。而当此文本消失时,意味着数据位于缓存中。当数据从缓存中返回时,系统未进行基础对象的调用,因此,没有执行数据库查询。
图12 :ObjectDataSource 在数据缓存中存储和检索数据
每个 ASP.NET 应用程序都有自己的数据缓存实例 , 所有页面和访问者可以共享该实例。这意味着,所有访问页面的用户也可以共享由 ObjectDataSource 在数据缓存中存储的数据。为验证这一点,我们在浏览器中打开 ObjectDataSource.aspx 页面。在首次访问该页面时,屏幕上出现 “Selecting event fired” 文本(假设由前面的测试添加到缓存的数据到此时已从缓存中清除了)。打开第二个浏览器实例,将第一个浏览器实例中的 URL 复制并粘贴到第二个实例中。在第二个浏览器实例中, “Selecting event fired” 文本没有显示,因为该实例使用的是第一个实例缓存的数据。
在向缓存中插入检索数据时 ,ObjectDataSource 使用一个缓存键值 , 该值包括 :CacheDuration 和 CacheExpirationPolicy 属性值 ;ObjectDataSource 使用的基础业务对象的类型 , 该类型通过 TypeName 属性 ( 本示例中为 ProductsBLL ) 指定 ;SelectMethod 属性的值以及 SelectParameters 参数集中参数的名称和参数值 ;StartRowIndex 和 MaximumRows 属性的值 , 这些属性在执行 自定义分页时使用。
将这些属性组合成一个缓存键值 , 可以确保在这些值发生改变时有唯一的缓存条目。例如,在以前的教程中,我们使用 ProductsBLL 类的 GetProductsByCategoryID(categoryID)来获取特定类别的所有产品信息。假设一位用户在页面查看饮料类 (CategoryID 为 1 ) 的产品信息。如果 ObjectDataSource 在缓存结果时没有考虑 SelectParameters 的值,当另一位用户登录页面查看调味品类的产品信息时,恰好饮料类产品的信息位于缓存中,第二位用户将会看到缓存的饮料类产品信息,而非他想查看的调味品信息。通过根据这些属性(其中包括 SelectParameters 的值)改变缓存键值, ObjectDataSource 会分别为饮料类和调味品类维护缓存条目。
过期数据问题
在调用 ObjectDataSource 的 Insert 、Update 或 Delete 方法时 ,ObjectDataSource 会自动将其项目从缓存中清除。这样做的好处是,当用户在页面上修改数据时,系统清除缓存条目,从而避免了过期数据的问题。然而,使用缓存的 ObjectDataSource 还是可能会显示过期数据。最简单的示例是直接在数据库中修改数据。例如,数据库管理员运行一个脚本在数据库中修改一些记录。
我们也可以对此情景进行一些细微的扩展。尽管在调用 ObjectDataSource 的数据修改方法时, ObjectDataSource 会将它的项目从缓存中清除,但清除的是与 ObjectDataSource 属性值( CacheDuration 、 TypeName 、 SelectMethod 等)的特定组合相匹配的缓存项目。如果我们有两个 ObjectDataSource ,它们使用不同的 SelectMethod 或 SelectParameter ,但可以更新相同的数据。但第一个 ObjectDataSource 更新一行记录并使自己的缓存条目失效时,第二个 ObjectDataSource 仍为相应的行应用缓存中的数据。下面,我们创建一些页面来展示该功能。首先,创建一个页面,在其中显示可编辑的 GridView 。该 GridView 对应的 ObjectDataSource 使用缓存并配置为通过 ProductsBLL 类的 GetProducts() 方法获取数据。再为本页面添加一个可编辑的 GridView 和一个 ObjectDataSource (或另外创建一个页面),但这个 ObjectDataSource 设置为使用 GetProductsByCategoryID(categoryID) 方法。由于两个 ObjectDataSource 的 SelectMethod 属性不同,因此他们拥有各自的缓存值。如果在第一个 GridView 中编辑一个产品,然后将数据重新绑定到第二个 GridView (通过分页、排序等),第二个 GridView 中显示的产品数据仍然是原有缓存数据,没有反映出第一个 GridView 作出的更改。
简而言之,如果能够接受存在过期数据的可能性,你可以只使用基于时间的到期机制,但须为重视数据时新性的场合设置较短的到期时间。如果无法接受陈旧的数据,你可以放弃缓存或使用 SQL 缓存依赖项(假设正在缓存数据库数据)。我们将在后面的教程中探讨 SQL 缓存依赖项。
小结
在本教程中,我们研究了 ObjectDataSource 的内置缓存功能。仅设置几个属性,我们就可以使 ObjectDataSource 将指定 SelectMethod 返回的结果缓存到 ASP.NET 数据缓存中。CacheDuration 和 CacheExpirationPolicy 属性分别指示缓存项目的时间段以及该时间段为固定值还是可变值。CacheKeyDependency 属性将 ObjectDataSource 的所有缓存条目与现有的缓存依赖项关联到一起。该属性(通常与 SQL 缓存依赖项一起使用)可用于在达到基于时间的期限之前,将 ObjectDataSource 的条目从缓存中清除。
由于 ObjectDataSource 只是将自己的数据缓存到数据缓存中,我们可通过编程复制 ObjectDataSource 的这种内置功能。在表示层这样做没有意义,因为 ObjectDataSource 已经在该层提供了这种功能。但我们可以在架构的一个独立的层中实现缓存功能。为此,我们需要重复 ObjectDataSource 使用的逻辑。在下一篇教程中,我们将讨论如何通过编程在架构内部使用数据缓存。
快乐编程!