Part 1
简介
使用 ObjectDataSource 缓存数据 和在架构中缓存数据 教程中探讨的缓存技术使用基于时间的有效期 , 在指定的周期过后从缓存中清除数据。该方法是平衡缓存性能与数据过时的最简单的方法。选择 x 秒的有效期,页面开发者虽然只能享受到 x 秒的缓存带来的好处,但可高枕无忧,因为数据的过期时间最多不会超过 x 秒。当然,对于静态数据, x 可以延长为 web 应用程序的生命周期,如 应用程序启动时缓存数据 教程所述。
缓存数据库数据时,人们常常会选用基于时间的有效期,因为其易于使用。但常常这不是个合适的方案。理想情况是:数据库数据一直被缓存,直到底层数据在数据库中被修改;此时才清除缓存。该方法能最大地获得缓存带来的性能上的好处,同时使过时数据保持的时间最短。然而,为享受到这些好处,必须建立一套系统,该系统可以感知到底层数据库数据发生了改变并从缓存中清除相应的条目。在 ASP.NET 2.0 以前,页面开发者负责实现该系统。
ASP.NET 2.0 提供了 SqlCacheDependency 类 以及必要的基础架构 , 利用它们可以在数据库发生了变化时感知到变化 , 从而清除相应的缓存条目。有两种技术可用于感知底层数据发生的变化:通知和轮询。下面我们会讨论通知和轮询的不同之处,之后,我们将创建必要的基础架构来支持轮询,然后探讨怎样通过声明和编程两种方式来使用 SqlCacheDependency 类。
了解通知和轮询
有两种技术可用于感知数据库中的数据发生的变化:通知和轮询。使用通知时,对于某个具体查询,如果自其上次执行以来其查询结果已发生了改变,数据库会自动通知 ASP.NET 运行时。使用轮询时,数据库服务器保存特定表最近发生更改时的信息。 ASP.NET 运行时周期性地查询数据库,看哪些表的数据在缓存后发生过改动。其数据改动过的那些表的相关缓存条目会被清除。
选用通知技术时,需要的建立工作比轮询少,并且具有更细的粒度,因为该技术在查询级而不是在表级跟踪变化。遗憾的是,只有在 Microsoft SQL Server 2005 的完整版,即非速成 (non-Express) 版中,才能使用通知。而对于 Microsoft SQL Server 从 7.0 到 2005 之间的所有版本 , 都可采用轮询技术。由于这些教程使用的是 SQL Server 2005 Express 版,我们将集中探讨建立和使用轮询。有关 SQL Server 2005 的通知功能的更多资料,请参考本教程末尾的更多阅读材料部分。
采用轮询时 , 需配置数据库 , 使其包含一个名为 AspNet_SqlCacheTablesForChangeNotification 的表 , 该表有三列– tableName 、notificationCreated 和changeId 。对于那些在 web 应用程序的 SQL 缓存依赖项中可能需要用到其数据的表,该表都有一条记录与之对应。 tableName 列指定表名,而 notificationCreated 指示将该记录添加到表中时的日期和时间。changeId 列为 int 类型 , 其初始值为0 。每次修改对应的表时 , 其值递增。
除了 AspNet_SqlCacheTablesForChangeNotification 表之外,数据库还需要对可能出现在 SQL 缓存依赖项中的每个表包含一个触发器。每当在 AspNet_SqlCacheTablesForChangeNotification 表中插入、更新、删除一条记录,或表中 changeId 值递增时,会执行这些触发器。
在使用一个SqlCacheDependency 对象缓存数据时 ,ASP.NET 运行时会跟踪相应表的当前 changeId 。系统周期性地检查数据库,一旦发现某个 SqlCacheDependency 对象的 changeId 值不同于数据库中的相应值,就清除该对象,因为 changeId 值不同意味着数据被缓存后相应表又有了变化。
步骤1 : 探讨aspnet_regsql.exe 命令行程序
如上所述 , 使用轮询方法时 , 必须对数据库进行设置 , 使其包含下面的基础架构 : 一个预先定义的表(AspNet_SqlCacheTablesForChangeNotification) , 一些存储过程 , 以及 web 应用程序中 SQL 缓存依赖项可能用到的各个表的触发器。可通过命令行程序 aspnet_regsql.exe 来创建这些表、存储过程和触发器,该程序位于 $WINDOWS$\Microsoft.NET\Framework\version 文件夹下。要创建 AspNet_SqlCacheTablesForChangeNotification 表和相关的存储过程,在命令行执行以下命令:
复制代码
/* For SQL Server authentication... */ aspnet_regsql.exe -S server -U user -P password -d database -ed /* For Windows Authentication... */ aspnet_regsql.exe -S server -E -d database -ed
注意 : 要执行这些命令 , 指定的数据库帐户必须具有 db_securityadmin 和 db_ddladmin 身份。要了解 aspnet_regsql.exe 命令行程序发送给数据库的T-SQL , 请参阅 此博客文章 。
例如 , 在Windows 身份认证模式下 , 对 Microsoft SQL Server 数据库服务器 ScottsServer 上的数据库 pubs 添加轮询的基础架构时 , 进入到相应的目录后在命令行中输入 :
aspnet_regsql.exe -S ScottsServer -E -d pubs -ed
完成数据库级基础架构的添加后 , 我们需要针对 SQL 缓存依赖项中要用到的那些表添加触发器。再次使用 aspnet_regsql.exe 命令行程序,不过这次使用 –t 来指定表名,且将 –ed 替换为 –et ,如下:
复制代码
/* For SQL Server authentication... */ aspnet_regsql.exe -S <i>server</i> -U <i>user</i> -P <i>password</i> -d <i>database</i> -t <i>tableName</i> -et /* For Windows Authentication... */ aspnet_regsql.exe -S <i>server</i> -E -d <i>database</i> -t <i>tableName</i> -et
要对ScottsServer 上的 pubs 数据库中的 authors 和 titles 表添加触发器 , 使用 :
复制代码
aspnet_regsql.exe -S ScottsServer -E -d pubs -t authors -et aspnet_regsql.exe -S ScottsServer -E -d pubs -t titles -et
对于本教程 , 要对Products 、Categories 和Suppliers 表添加触发器。我们将在步骤 3 中探讨具体的命令行语句。
步骤2 : 在App_Data 中引用Microsoft SQL Server Express 版数据库
在添加必要的轮询基础架构时 ,spnet_regsql.exe 命令行程序需要数据库和服务器的名称。但是对于放在 App_Data 文件夹中的 Microsoft SQL Server 2005 Express 数据库而言,它的数据库名和服务器名又是什么呢?没有必要探究其数据库名和服务器名到底是什么,我发现最简单的方法是用 SQL Server Management Studio 来将该数据库附加到 localhost\SQLExpress 数据库实例,并重命名该数据库。如果您在计算机上安装了 SQL Server 2005 的一个完整版,则很可能也安装了 SQL Server Management Studio 。 如果您安装的是 Express 版本,可以下载免费的 Microsoft SQL Server Management Studio Express Edition 。
首先 , 关闭Visual Studio 。然后 , 打开 SQL Server Management Studio , 选择使用 Windows Authentication 连接到 localhost\SQLExpress 服务器。
图1 : 连接到 localhost\SQLExpress 服务器
连接到服务器后 ,Management Studio 将显示该服务器 ,其中有 针对数据库、安全等的子文件夹。右键单击 Databases 文件夹并选择 Attach 选项。这将弹出 Attach Databases 对话框(参见图 2 )。单击 Add 按钮,选择您的 web 应用程序的 App_Data 文件夹下的 NORTHWND.MDF 数据库文件夹。
图2 : 附加 App_Data 文件夹下的 NORTHWND.MDF 数据库
这会将该数据库添加到 Databases 文件夹。数据库名可能是该数据库文件的完整路径或前面带有一个 GUID 的完整路径。为避免在使用 aspnet_regsql.exe 命令行工具时输入该过长的数据库名,可通过右键单击刚附加的数据库并选择 Rename 将该数据库重命名为一个更友好的名称。我将该数据库重命名为 “DataTutorials” 。
图3 : 将附加的数据库重命名为更友好的名称
步骤3 : 对Northwind 数据库添加轮询基础架构
现在我们已附加了 App_Data 文件夹下的 NORTHWND.MDF 数据库 , 可以开始添加轮询基础架构了。假定您已将该数据库重命名为 “DataTutorials” ,执行如下四个命令:
复制代码
aspnet_regsql.exe -S localhost\SQLExpress -E -d DataTutorials -ed aspnet_regsql.exe -S localhost\SQLExpress -E -d DataTutorials -t Products -et aspnet_regsql.exe -S localhost\SQLExpress -E -d DataTutorials -t Categories -et aspnet_regsql.exe -S localhost\SQLExpress -E -d DataTutorials -t Suppliers -et
执行这四个命令之后 , 在Management Studio 中右键单击该数据库名 , 进入Tasks 子菜单 , 选择Detach 。然后关闭 Management Studio ,重新打开 Visual Studio 。
重新打开 Visual Studio 后,在 Server Explorer 中找到并展开该数据库。其中,可看到一个新表 (AspNet_SqlCacheTablesForChangeNotification) 、一些新的存储过程以及 Products 、 Categories 和 Suppliers 表的触发器。
图4 : 数据库现在包含必需的轮询基础架构
步骤4 : 配置轮询服务
在数据库中创建了所需的表、触发器和存储过程后 , 最后步骤是配置轮询服务 , 为此 , 在 Web.config 中指定要使用的数据库 , 以及轮询频率 ( 单位为毫秒 ) 。下面的标记代码指定每隔 1 秒轮询一次 Northwind 数据库。
复制代码
<?xml version="1.0"?> <configuration> <connectionStrings> <add name="NORTHWNDConnectionString" connectionString= "Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\NORTHWND.MDF; Integrated Security=True;User Instance=True" providerName="System.Data.SqlClient"/> </connectionStrings> <system.web> ... <!-- Configure the polling service used for SQL cache dependencies --> <caching> <sqlCacheDependency enabled="true" pollTime="1000" > <databases> <add name="NorthwindDB" connectionStringName="NORTHWNDConnectionString" /> </databases> </sqlCacheDependency> </caching> </system.web> </configuration>
<add> 元素中的 name 值 (“NorthwindDB”) 是一个易读的名称 , 它对应于一个具体的数据库。使用 SQL 缓存依赖项时,我们需要引用此处定义的数据库名称以及缓存数据所依赖的表。在步骤 6 中,我们将看到怎样使用 SqlCacheDependency 类通过编码将 SQL 缓存依赖项与缓存数据相关联。
一旦建立了SQL 缓存依赖项 , 轮询系统将每隔 pollTime 毫秒连接到 <databases> 元素中定义的数据库 , 并执行AspNet_SqlCachePollingStoredProcedure 存储过程。该存储过程是在步骤 3 中使用 aspnet_regsql.exe 命令行工具添加的,它会返回 AspNet_SqlCacheTablesForChangeNotificationeturns 表中每条记录的 tableName 和 changeId 值。过时的 SQL 缓存依赖项会从缓存中清除。
应在权衡性能和数据过时的基础上设置 pollTime 。小的 pollTime 值虽然使对数据库的请求次数增加,但能更快的将过时数据从缓存中清除。较大的 pollTime 值虽然减少了对数据库的请求次数,但增加了从后台数据改变到清除相关缓存条目之间的延迟时间。还好,数据库请求只是执行一个简单的存储过程,该存储过程只是从一个简单的记录数少的表返回不多的几行。对您的应用程序试用一些不同的 pollTime 值,从中找出一个可平衡数据库访问和数据过时这两者的理想值。允许的最小 pollTime 值为 500 。
注意 : 上面的例子在 <sqlCacheDependency> 元素中指定了一个单一的 pollTime ,但您也可以随意地在 <add> 元素中指定 pollTime 值。当您指定了多个数据库且想为每个数据库都指定一个轮询频率时,这样做很有用。
步骤5 : 通过声明方式使用 SQL 缓存依赖项
在步骤 1 到 4 中,我们探讨了如何建立必需的数据库基础架构并配置轮询系统。有了这个基础架构后,现在我们可以通过编程或声明技术,在添加条目到数据缓存时使用相关的 SQL 缓存依赖项。在本步骤中,我们将探讨如何以声明的方式使用 SQL 缓存依赖项。在步骤 6 中,我们将探讨编程的方式。
使用 ObjectDataSource 缓存数据教程探讨了 ObjectDataSource 的声明方式的缓存功能。只要简单地将 EnableCaching 属性设置为 True , CacheDuration 属性设置为某个时间间隔, ObjectDataSource 就会自动地以指定间隔缓存从其底层对象返回的数据。 ObjectDataSource 还可使用一个或多个 SQL 缓存依赖项。
下面我们演示怎样以声明方式使用 SQL 缓存依赖项。打开 Caching 文件夹中的 SqlCacheDependencies.aspx 页面,从 Toolbox 中将一个 GridView 控件拖放到设计器中。将该 GridView 的 ID 设置为 ProductsDeclarative ,从其智能标签中选择相应选项,将其绑定到一个名为 ProductsDataSourceDeclarative 的新 ObjectDataSource 。
图5 : 创建一个名为 ProductsDataSourceDeclarative 的新ObjectDataSource
配置该ObjectDataSource 使用 ProductsBLL 类 , 在SELECT 选项卡的下拉列表中选择 GetProducts() 。在 UPDATE 选项卡中,选择有三个输入参数 – productName 、 unitPrice 和 productID 的 UpdateProduct 重载方法。在 INSERT 和 DELETE 选项卡的下拉列表中选择 “(None)” 。
图6 : 选择有三个输入参数的 UpdateProduct 重载方法
图7 : 在 INSERT 和 DELETE 选项卡的下拉列表中选择“(None)”
完成Configure Data Source 向导后 ,Visual Studio 将为每个数据字段在GridView 中创建 BoundField 和CheckBoxField 。将 ProductName 、 CategoryName 和 UnitPrice 之外的所有字段删除,并按您的意愿格式化这些字段。在 GridView 的智能标签中,选中 Enable Paging 、 Enable Sorting 和 Enable Editing 复选框。 Visual Studio 会将 ObjectDataSource 的 OldValuesParameterFormatString 属性设置为 original_{0} 。为使 GridView 的编辑功能正常工作,要么从声明语句中完全地删除该属性,要么将其设置为缺省值 {0} 。
最后 , 在 GridView 上面添加一个 Label Web 控件 , 并将其 ID 属性设置为 ODSEvents , 其EnableViewState 属性设置为 False 。完成这些改变后,页面的声明代码应类似如下。注意,我对 GridView 的各字段的外观进行了一些定制,这对于演示 SQL 缓存依赖项功能来说并不是必要的。
复制代码
<asp:Label ID="ODSEvents" runat="server" EnableViewState="False" /> <asp:GridView ID="ProductsDeclarative" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsDataSourceDeclarative" 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:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="ProductName" Display="Dynamic" ErrorMessage="You must provide a name for the product." SetFocusOnError="True" runat="server">*</asp:RequiredFieldValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>' /> </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" runat="server" ControlToValidate="UnitPrice" 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" Display="Dynamic" 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="ProductsDataSourceDeclarative" runat="server" 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>
Part 2
接下来 , 为ObjectDataSource 的Selecting 事件创建一个事件处理器 , 并在其中添加如下代码 :
复制代码
protected void ProductsDataSourceDeclarative_Selecting (object sender, ObjectDataSourceSelectingEventArgs e) { ODSEvents.Text = "-- Selecting event fired"; }
我们知道 , 只有当ObjectDataSource 从其底层对象获取数据时 , 才会触发它的 Selecting 事件。如果 ObjectDataSource 从自己的缓存中访问数据,则不会触发该事件。
现在从浏览器访问该页面。由于我们还未实现缓存,每次对网格进行分页、排序或编辑时,页面都会显示文本 “Selecting event fired” ,如图 8 所示。
图8 : 每次对 GridView 进行分页、编辑或排序时触发 ObjectDataSource 的 Selecting 事件
在使用 ObjectDataSource 缓存数据 教程中讲过 , 如果 EnableCaching 属性设置为 true ,ObjectDataSource 会将数据缓存 , 并在 CacheDuration 属性指定的时间内维持缓存数据。 ObjectDataSource 还有一个 SqlCacheDependency 属性 ,该属性可以通过以下模式为缓存数据添加一个或多个 SQL 缓存依赖项:
databaseName1:tableName1;databaseName2:tableName2;...
其中 ,databaseName 是Web.config 中 <add> 元素的name 属性中指定的数据库名 ,tableName 为数据库表名。例如,要创建这样一个 ObjectDataSource ,它使用基于 Northwind 的 Products 表的 SQL 缓存依赖项来缓存数据,缓存数据的时间不限,我们需将 ObjectDataSource 的 EnableCaching 属性设置为 True ,其 SqlCacheDependency 属性设置为 “NorthwindDB:Products” 。
注意 : 可以同时使用 SQL 缓存依赖项和基于时间的有效期 , 方法是 , 将 EnableCaching 设置为 True 、CacheDuration 设置为所需时间间隔、SqlCacheDependency 设置为数据库名和表名。不管是基于时间的有效期到期还是轮询系统发现底层数据库数据发生改变, ObjectDataSource 都会清除其数据。
SqlCacheDependencies.aspx 页面中的 GridView 显示的数据来自两张表 - Products 和 Categories ( 产品的 CategoryName 字段通过 JOIN on Categories 获得 ) 。因此,我们想指定两个 SQL 缓存依赖项: “NorthwindDB:Products;NorthwindDB:Categories” 。
图9 : 配置 ObjectDataSource 支持缓存且使用基于 Products 和 Categories 的 SQL 缓存依赖项
配置ObjectDataSource 支持缓存后 , 通过浏览器再次访问该页面。首次访问页面时 , 会出现 “Selecting event fired” 文本 , 但进行翻页、排序或单击 Edit 或 Cancel 按钮时 , 该文本会消失。这是因为数据装载到 ObjectDataSource 的缓存中后,它一直在那儿,直到 Products 或 Categories 表发生改变或通过 GridView 对数据进行了更新。
翻页显示网格 , 此时“Selecting event fired” 没有出现。接着 , 另外打开一个新的浏览器窗口并导航到Basics 教程的编辑、插入和删除部分中的 Basics.aspx 页面 (~/EditInsertDelete/Basics.aspx) 。更新某产品的名称或价格。然后,从第一个浏览器窗口查看另一个不同的数据页、对网格进行排序或单击某行的 Edit 按钮。这次, “Selecting event fired” 又出现了,因为底层数据库数据已更改(参见图 10 )。如果该文本没有出现,稍等后重试。我们知道,轮询服务每隔 pollTime 毫秒检测一次 Products 表的变化,因此在底层数据的更新和缓存数据的清除之间有一个延迟。
图10 : 修改 Products 表会清除缓存的产品数据
步骤6 : 通过编程方式使用SqlCacheDependency 类
在架构中缓存数据教程讲述了在架构中使用一个单独的缓存层的好处 ,这胜过了缓存功能与 ObjectDataSource 紧密结合的情况。在那个教程中,我们创建了一个 ProductsCL 类来演示怎样以编程的方式进行数据缓存。要在缓存层使用 SQL 缓存依赖项,要使用 SqlCacheDependency 类。
在轮询系统中 , 一个SqlCacheDependency 对象必须与某个具体的数据库和表相关联。例如,下面的代码创建了一个基于 Northwind 数据库的 Products 表的 SqlCacheDependency 对象:
复制代码
Caching.SqlCacheDependency productsTableDependency = new Caching.SqlCacheDependency("NorthwindDB", "Products");
SqlCacheDependency 的构造函数的两个输入参数分别是数据库名和表名。与ObjectDataSource 的 SqlCacheDependency 属性类似 , 使用的数据库名是 Web.config 文件中 <add> 元素的 name 属性指定的值。表名是实际的数据库表名。
要将一个 SqlCacheDependency 与一个添加到数据缓存中的条目相关联 , 可以使用一个接受依赖项的 Insert 重载方法。下面的代码将 value 添加到数据缓存中,缓存时间不限,并将它基于 Products 表的 SqlCacheDependency 相关联。换言之,value 会一直保持在缓存中,直到由于内存不足或轮询系统检测到 Products 表在缓存后发生了改变才被清除。
复制代码
Caching.SqlCacheDependency productsTableDependency = new Caching.SqlCacheDependency("NorthwindDB", "Products"); Cache.Insert(key, value, productsTableDependency, System.Web.Caching.Cache.NoAbsoluteExpiration, System.Web.Caching.Cache.NoSlidingExpiration);
目前 , 缓存层的ProductsCL 类缓存 Products 表的数据 , 基于时间的有效期为 60 秒。我们对该类进行更新 , 使其使用 SQL 缓存依赖项。 ProductsCL 类的 AddCacheItem 方法负责将数据添加到缓存,目前它包含下面的代码:
复制代码
private void AddCacheItem(string rawKey, object value) { System.Web.Caching.Cache DataCache = HttpRuntime.Cache; // Make sure MasterCacheKeyArray[0] is in the cache DataCache[MasterCacheKeyArray[0]] = DateTime.Now; // Add a CacheDependency Caching.CacheDependency dependency = new Caching.CacheDependency(null, MasterCacheKeyArray); DataCache.Insert(GetCacheKey(rawKey), value, dependency, DateTime.Now.AddSeconds(CacheDuration), System.Web.Caching.Cache.NoSlidingExpiration); }
更新该代码 , 用一个SqlCacheDependency 对象来替换掉MasterCacheKeyArray 缓存依赖项:
复制代码
private void AddCacheItem(string rawKey, object value) { System.Web.Caching.Cache DataCache = HttpRuntime.Cache; // Add the SqlCacheDependency objects for Products Caching.SqlCacheDependency productsTableDependency = new Caching.SqlCacheDependency("NorthwindDB", "Products"); // Add the item to the data cache using productsTableDependency DataCache.Insert(GetCacheKey(rawKey), value, productsTableDependency, Caching.Cache.NoAbsoluteExpiration, Caching.Cache.NoSlidingExpiration); }
为了测试该功能 , 在页面已有的ProductsDeclarative GridView 下面另外添加一个 GridView 。将该新 GridView 的 ID 设置为 ProductsProgrammatic , 通过其智能标签将其绑定到一个新的名为ProductsDataSourceProgrammatic 的ObjectDataSource 。配置该 ObjectDataSource 使用 ProductsCL 类,在 SELECT 和 UPDATE 选项卡的下拉列表中分别选择 GetProducts 和 UpdateProduct 。
图11 : 配置 ObjectDataSource 使用 ProductsCL 类
图12 : 在 SELECT 选项卡的下拉列表中选择 GetProducts 方法
图13 : 在 UPDATE 选项卡的下拉列表中选择 UpdateProduct 方法
完成Configure Data Source 向导后 ,Visual Studio 将为每个数据字段在GridView 中创建 BoundField 和CheckBoxField 。与添加到该页的第一个 GridView 一样,将 ProductName 、 CategoryName 和 UnitPrice 之外的所有字段删除,并按照您的意愿格式化这些字段。在 GridView 的智能标签中,选中 Enable Paging 、 Enable Sorting 和 Enable Editing 复选框。与 ProductsDataSourceDeclarative ObjectDataSource 一样, Visual Studio 会将 ProductsDataSourceProgrammatic ObjectDataSource 的 OldValuesParameterFormatString 属性设置为 original_{0} 。为使 GridView 的编辑功能正常工作,将该属性置回 {0} (或从声明语句中完全地删除该属性赋值)。
完成这些任务后 ,最终 的 GridView 和 ObjectDataSource 声明代码类似如下 :
复制代码
<asp:GridView ID="ProductsProgrammatic" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsDataSourceProgrammatic" 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:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="ProductName" Display="Dynamic" ErrorMessage="You must provide a name for the product." SetFocusOnError="True" runat="server">*</asp:RequiredFieldValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>' /> </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" runat="server" 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" 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="ProductsDataSourceProgrammatic" runat="server" OldValuesParameterFormatString="{0}" SelectMethod="GetProducts" TypeName="ProductsCL" UpdateMethod="UpdateProduct"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> </asp:ObjectDataSource>
为测试缓存层中的 SQL 缓存依赖项, 在 ProductCL 类的 AddCacheItem 方法中设置一个断点 , 然后启动调试。首次访问 SqlCacheDependencies.aspx 时,会触发断点,因为是首次请求数据,且数据会被放入缓存中。接下来,在 GridView 中跳转到另一页或对某个列进行排序。这会导致 GridView 查询数据,由于 Products 数据库表尚未改动,会在缓存中找到数据。如果反复在缓存中找不到数据,确保您机器上有足够可用的内存,然后重试。
在GridView 里跳转几页, 打开另一个浏览器窗口 , 导航到Basics 教程的 编辑 、 插入 和 删除 部分中的 Basics.aspx 页面(~/EditInsertDelete/Basics.aspx) 。更新 Products 表的一条记录,然后从第一个浏览器窗口查看一个新页或单击一个排序标题。
此时,会遇到以下两种情况之一 : 要么触发断点 , 这意味着缓存数据由于数据库发生变化而被清除 ; 要么不触发断点 , 意味着SqlCacheDependencies.aspx 显示的是过时的数据。如果没有触发断点,很可能是由于数据变化后还未触发轮询服务。我们知道,轮询服务每隔 pollTime 毫秒检测一次 Products 表的变化,因此在底层数据的更新和缓存数据的清除之间有一个延迟。
注意 : 通过 SqlCacheDependencies.aspx 中的GridView 编辑某个产品时很可能会产生该延迟。 在架构中缓存数据教程中 , 我们添加了MasterCacheKeyArray 缓存依赖项以确保通过ProductsCL 类的 UpdateProduct 方法编辑的数据会从缓存中清除。然而在本步骤开始时,我们在修改 AddCacheItem 方法时替换了该缓存依赖项,因此 ProductsCL 类会继续显示缓存的数据,直到轮询系统发现 Products 表的变化。我们将在步骤 7 中了解怎样重新引入 MasterCacheKeyArray 缓存依赖项。
步骤7 : 将一个缓存条目与多个依赖项相关联
我们知道 ,MasterCacheKeyArray 缓存依赖项用于确保在缓存中 与产品相关的所有条目中 , 只要其中一条的相关数据发生更改 , 所有 与产品相关的数据都会从缓存清除。例如, GetProductsByCategoryID(categoryID) 方法对每个独特的 categoryID 值缓存多个 ProductsDataTables 实例。如果其中一个对象被清除, MasterCacheKeyArray 缓存依赖项会确保其它对象也被清除。如果没有该缓存依赖项,就会存在这种可能性,当缓存的数据被修改后,其它缓存的产品数据可能过时。因此,在使用 SQL 缓存依赖项的时候保持 MasterCacheKeyArray 缓存依赖项是很重要的。然而,数据缓存的 Insert 方法只允许一个依赖项对象。
此外 , 在使用SQL 缓存依赖项 时 , 我们可能要依赖多个数据库表。例如,在 ProductsCL 类中缓存的 ProductsDataTable 包含每个产品的分类名称和供应商名称,但 AddCacheItem 方法只使用一个基于 Products 表的依赖项。此种情况下,如果用户更新了分类或供应商的名称,则缓存的产品数据虽然已过时,仍然会驻留在缓存中。因此,我们想使缓存的产品数据不仅依赖 Products 表,还要依赖 Categories 和 Suppliers 表。
AggregateCacheDependency类 提供了这样一个途径 , 可将一个缓存条目与多个 依赖项 关联起来。首先,创建一个 AggregateCacheDependency 实例。然后使用 AggregateCacheDependency 的 Add 方法添加这些依赖项。其后,在将条目插入到数据缓存的同时,传入 AggregateCacheDependency 实例。当 AggregateCacheDependency 实例的任何一个依赖项发生变化时,该缓存条目就会被清除。
下面给出ProductsCL 类的 AddCacheItem 方法的更新后的代码。该方法不仅创建了MasterCacheKeyArray 缓存依赖项 , 还创建了分别基于 Products 、Categories 和 Suppliers 表的SqlCacheDependency 对象。把它们组合起来构成一个名为 aggregateDependencies 的 AggregateCacheDependency 对象,然后将该对象传递给 Insert 方法。
复制代码
private void AddCacheItem(string rawKey, object value) { System.Web.Caching.Cache DataCache = HttpRuntime.Cache; // Make sure MasterCacheKeyArray[0] is in the cache and create a depedency DataCache[MasterCacheKeyArray[0]] = DateTime.Now; Caching.CacheDependency masterCacheKeyDependency = new Caching.CacheDependency(null, MasterCacheKeyArray); // Add the SqlCacheDependency objects for Products, Categories, and Suppliers Caching.SqlCacheDependency productsTableDependency = new Caching.SqlCacheDependency("NorthwindDB", "Products"); Caching.SqlCacheDependency categoriesTableDependency = new Caching.SqlCacheDependency("NorthwindDB", "Categories"); Caching.SqlCacheDependency suppliersTableDependency = new Caching.SqlCacheDependency("NorthwindDB", "Suppliers"); // Create an AggregateCacheDependency Caching.AggregateCacheDependency aggregateDependencies = new Caching.AggregateCacheDependency(); aggregateDependencies.Add(masterCacheKeyDependency, productsTableDependency, categoriesTableDependency, suppliersTableDependency); DataCache.Insert(GetCacheKey(rawKey), value, aggregateDependencies, Caching.Cache.NoAbsoluteExpiration, Caching.Cache.NoSlidingExpiration); }
对新代码进行测试。现在 , 更改Products 、Categories 或Suppliers 表时会清除缓存的数据。此外,通过 GridView 编辑某产品时会调用 ProductsCL 类的 UpdateProduct 方法,该方法清除 MasterCacheKeyArray 缓存依赖项,进而导致清除缓存的 ProductsDataTable ,下次请求时将重新从数据库提取数据。
注意 :SQL 缓存依赖项也可用于输出缓存 。有关该功能的说明,参见: 对 SQL Server 使用 ASP.NET 输出缓存 .
小结
缓存数据库数据时 ,理想的情况是 数据一直保存在缓存中 , 直到在数据库中该数据发生了改变。在 ASP.NET 2.0 中,可通过声明和编程两种方式来创建和使用 SQL 缓存依赖项。该方法面临的一个挑战是及时发现数据的改动。 Microsoft SQL Server 2005 的完整版提供通知功能,可在查询结果变化时通知应用程序。对于 SQL Server 2005 Express 版和旧版本的 SQL Server ,必须使用轮询系统。不过还好,建立必要的轮询基础架构很简单。