使用 SQL 缓存依赖项 (C#)

作者 :斯科特·米切尔

下载 PDF

最简单的缓存策略是允许缓存的数据在指定时间段后过期。 但是,这种简单的方法意味着缓存的数据与其基础数据源没有关联,结果是会导致过时数据被保留过久,或当前数据过快地过期。 更好的方法是使用 SqlCacheDependency 类,以便数据在 SQL 数据库中修改其基础数据之前保持缓存状态。 本教程介绍如何进行。

介绍

在教程 ObjectDataSource 的数据缓存体系结构中的数据缓存 中检查的缓存技术使用了基于时间的失效机制,以在指定时间段后从缓存中移除数据。 此方法是平衡缓存性能提升与数据陈旧问题的最简单方法。 通过选择 x 秒的过期时间,页面开发人员只能享受缓存 x 秒所带来的性能优势,但可以放心地确保数据不过期时间最长不会超过 x 秒。 当然,对于静态数据, x 可以扩展到 Web 应用程序的生存期,如 应用程序启动时缓存数据 教程中所述。

缓存数据库数据时,通常会选择基于时间的到期时间,使其易于使用,但通常是一个不足的解决方案。 理想情况下,数据库数据在未被修改时会保持缓存状态;只有当数据库中的基础数据被修改后,才会逐出缓存。 此方法可最大程度地提高缓存的性能优势,并最大程度地减少过时数据的持续时间。 但是,为了享受这些好处,必须有一些系统知道基础数据库数据何时被修改并逐出缓存中的相应项。 在 ASP.NET 2.0 之前,页面开发人员负责实现此系统。

ASP.NET 2.0 提供了一个 SqlCacheDependency 和必要的基础结构,用于确定数据库中发生更改的时间,以便可以逐出相应的缓存项。 有两种方法可用于确定基础数据何时发生更改:通知和轮询。 讨论通知和轮询之间的差异后,我们将创建支持轮询所需的基础结构,然后探索如何在声明性和编程方案中使用该 SqlCacheDependency 类。

理解通知与轮询

有两种方法可用于确定数据库中的数据何时修改:通知和轮询。 使用通知时,数据库会在自上次执行查询以来更改特定查询的结果时自动向 ASP.NET 运行时发出警报,此时会逐出与查询关联的缓存项。 通过轮询,数据库服务器会保留有关上次更新特定表的时间的信息。 ASP.NET 运行时会定期轮询数据库,以检查哪些表已更改,因为它们已输入到缓存中。 那些数据已被修改的表,其关联的缓存项已被清除。

通知选项需要的设置比轮询要少,而且更精细,因为它跟踪查询级别的更改,而不是在表级别跟踪更改。 遗憾的是,通知仅在完整版本的 MICROSOFT SQL Server 2005(即非 Express 版本)中提供。 但是,轮询功能可用于从 7.0 到 2005 的所有版本的 Microsoft SQL Server。 由于这些教程使用 SQL Server 2005 的 Express 版本,因此我们将重点介绍如何设置和使用轮询选项。 有关 SQL Server 2005 通知功能上的更多资源,请参阅本教程末尾的“进一步阅读”部分。

通过轮询,必须将数据库配置为包含一个表AspNet_SqlCacheTablesForChangeNotification,该表具有三列:tableNamenotificationCreatedchangeId。 此表包含每个表的行,其中包含可能需要在 Web 应用程序中的 SQL 缓存依赖项中使用的数据。 该 tableName 列指定表的名称,同时 notificationCreated 指示行添加到表中的日期和时间。 changeId 列的类型为 int,初始值为 0。 其值随对表的每个修改而递增。

除了 AspNet_SqlCacheTablesForChangeNotification 表,数据库还需要在可能出现在 SQL 缓存依赖项中的每个表上包括触发器。 每当插入、更新或删除行并递增表 changeIdAspNet_SqlCacheTablesForChangeNotification时,就会执行这些触发器。

ASP.NET 运行时在使用SqlCacheDependency对象缓存数据时,会跟踪表的当前changeId。 定期检查数据库,任何其 changeId 与数据库中值不同的 SqlCacheDependency 对象将被逐出,因为不同的 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_securityadmindb_ddladmin角色中。

例如,若要将用于轮询的基础结构添加到命名为pubs的 Microsoft SQL Server 数据库,并使用 Windows 身份验证连接到名为ScottsServer的数据库服务器,请导航到相应的目录,然后在命令行中输入:

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

若要将触发器添加到authorstitles表上,使用pubs数据库中的ScottsServer

aspnet_regsql.exe -S ScottsServer -E -d pubs -t authors -et
aspnet_regsql.exe -S ScottsServer -E -d pubs -t titles -et

对于本教程,请将触发器添加到ProductsCategoriesSuppliers表。 我们将了解步骤 3 中的特定命令行语法。

步骤 2:在某个位置引用 Microsoft SQL Server 2005 Express Edition 数据库App_Data

aspnet_regsql.exe命令行程序需要数据库和服务器名称才能添加必要的轮询基础结构。 驻留在 App_Data 文件夹中的 Microsoft SQL Server 2005 Express 数据库的名称和服务器名称是什么? 我发现,最简单的方法是将数据库附加到 localhost\SQLExpress 数据库实例,并使用 SQL Server Management Studio 重命名数据,而无需发现数据库名称和服务器名称。 如果计算机上安装了 SQL Server 2005 的完整版本之一,则可能已在计算机上安装了 SQL Server Management Studio。 如果只有 Express 版本,则可以下载免费 Microsoft SQL Server Management Studio Express Edition

首先关闭 Visual Studio。 接下来,打开 SQL Server Management Studio 并选择使用 Windows 身份验证连接到 localhost\SQLExpress 服务器。

附加到 localhost\SQLExpress Server

图 1:附加到 localhost\SQLExpress 服务器

连接到服务器后,Management Studio 将显示服务器,并具有数据库、安全性等子文件夹。 右键单击“数据库”文件夹,然后选择“附加”选项。 此时会显示“附加数据库”对话框(请参阅图 2)。 单击“添加”按钮,然后选择 NORTHWND.MDF Web 应用程序文件夹中 App_Data 的数据库文件夹。

从 App_Data 文件夹中附加 NORTHWND.MDF 数据库

图 2:从App_Data文件夹附加NORTHWND.MDF数据库(单击以查看全尺寸图像

这会将数据库添加到“数据库”文件夹。 数据库名称可能是数据库文件的完整路径,也可能是 GUID 前面附加的完整路径。 若要避免在使用 aspnet_regsql.exe 命令行工具时键入此冗长的数据库名称,请右键单击刚附加的数据库并选择“重命名”,将数据库重命名为更人性化的名称。 我已将数据库重命名为 DataTutorials。

将附加的数据库重命名为一个更具 Human-Friendly 特征的名称

图 3:将附加的数据库重命名为符合 Human-Friendly 类型的名称

步骤 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 中的数据库名称,转到“任务”子菜单,然后选择“分离”。 然后关闭 Management Studio 并重新打开 Visual Studio。

Visual Studio 重新打开后,通过服务器资源管理器访问数据库。 记下新表(AspNet_SqlCacheTablesForChangeNotification)、新的存储过程以及表上的ProductsCategoriesSuppliers触发器。

数据库现在包括必要的轮询基础结构

图 4:数据库现在包括必要的轮询基础结构

步骤 4:配置轮询服务

在数据库中创建所需的表、触发器和存储过程后,最后一步是配置轮询服务,这是通过 Web.config 指定要使用的数据库以及轮询频率(以毫秒为单位)来完成的。 以下标记每秒轮询一次 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>

name元素 (NorthwindDB) 中的<add>值将人类可读的名称与特定数据库相关联。 使用 SQL 缓存依赖项时,需要引用此处定义的数据库名称以及缓存数据所基于的表。 我们将了解如何使用 SqlCacheDependency 类以编程方式将 SQL 缓存依赖项与步骤 6 中的缓存数据相关联。

建立 SQL 缓存依赖项后,轮询系统将连接到元素中 <databases> 定义的数据库,每 pollTime 毫秒执行 AspNet_SqlCachePollingStoredProcedure 存储过程。 此存储过程(使用aspnet_regsql.exe命令行工具在步骤 3 中添加)返回tableNamechangeIdAspNet_SqlCacheTablesForChangeNotification中每个记录的值。 过时的 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 缓存依赖项,请打开 SqlCacheDependencies.aspx 文件夹中的页面 Caching ,并将 GridView 从工具箱拖动到设计器上。 将 GridView 的ID设置为ProductsDeclarative,并从其智能标记中选择将其绑定到名为ProductsDataSourceDeclarative的新 ObjectDataSource。

创建名为 ProductsDataSourceDeclarative 的 ObjectDataSource

图 5:创建名为 ProductsDataSourceDeclarative 的新 ObjectDataSource (单击可查看全尺寸图像

将 ObjectDataSource 配置为使用 ProductsBLL 类,并将 SELECT 选项卡中的下拉列表设置为 GetProducts()。 在“更新”选项卡中,选择具有三个输入参数productNameunitPriceproductID的重载。 在 INSERT 和 DELETE 选项卡中将下拉列表设置为“无”。

将 UpdateProduct 重载与三个输入参数配合使用

图 6:将 UpdateProduct 重载与三个输入参数配合使用(单击可查看全尺寸图像

将 INSERT 和 DELETE 选项卡的 Drop-Down 列表设置为“无”

图 7:将 INSERT 和 DELETE 选项卡的 Drop-Down 列表设置为“无”(单击以查看全尺寸图像

完成“配置数据源”向导后,Visual Studio 将在 GridView 中为每个数据字段创建 BoundFields 和 CheckBoxFields。 删除除了ProductNameCategoryNameUnitPrice之外的所有字段,并根据你的判断设置这些字段的格式。 在 GridView 智能标记中,选中“启用分页”、“启用排序”和“启用编辑”复选框。 Visual Studio 会将 ObjectDataSource s OldValuesParameterFormatString 属性设置为 original_{0}. 为了使 GridView 的编辑功能正常工作,请完全从声明性语法中删除此属性,或将其设置回其默认值 {0}

最后,在 GridView 上方添加一个标签 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>

接下来,为 ObjectDataSource 事件 Selecting 创建事件处理程序,并在其中添加以下代码:

protected void ProductsDataSourceDeclarative_Selecting
    (object sender, ObjectDataSourceSelectingEventArgs e)
{
    ODSEvents.Text = "-- Selecting event fired";
}

回想一下,仅当从其基础对象检索数据时,ObjectDataSource 事件 Selecting 才会触发。 如果 ObjectDataSource 从其自己的缓存访问数据,则不会触发此事件。

现在,通过浏览器访问此页面。 由于我们尚未实现任何缓存,因此每次分页、排序或编辑网格时,页面都应该显示文本“选择事件已触发”,如图 8 所示。

每次对 GridView 进行分页、编辑或排序时,ObjectDataSource 的选择事件都会触发

图 8:每次 GridView 分页、编辑或排序时,ObjectDataSource 的事件 Selecting 都会触发(单击以查看全尺寸图像

正如我们在使用 ObjectDataSource 教程的缓存数据中看到的那样,将属性true设置为EnableCaching使 ObjectDataSource 在其属性指定的持续时间内缓存其CacheDuration数据。 ObjectDataSource 还有一个 SqlCacheDependency 属性,该属性使用模式向缓存的数据添加一个或多个 SQL 缓存依赖项:

databaseName1:tableName1;databaseName2:tableName2;...

其中 databaseName 是在元素Web.config的属性中指定的name<add>数据库的名称,tableName 是数据库表的名称。 例如,若要创建一个 ObjectDataSource,使其基于 Northwind 的 Products 表的 SQL 缓存依赖项来无限期缓存数据,请将 ObjectDataSource 的 EnableCaching 属性设置为 true,并将 SqlCacheDependency 属性设置为 NorthwindDB:Products。

注释

可以通过设置 SQL 缓存依赖关系和基于时间的过期规则,在EnableCaching中指定true、在CacheDuration中指定时间间隔以及在SqlCacheDependency中指定数据库和表名称来实现。 当达到基于时间的到期时间或轮询系统注意到基础数据库数据已更改时,ObjectDataSource 将逐出其数据,以首先发生者为准。

网格视图SqlCacheDependencies.aspx中的数据显示来自两个表——ProductsCategories(产品的CategoryName字段是通过JOINCategories上检索的)。 因此,我们需要指定两个 SQL 缓存依赖项:NorthwindDB:Products;NorthwindDB:Categories.

配置 ObjectDataSource 以支持对产品和类别使用 SQL 缓存依赖项进行缓存

图 9:配置 ObjectDataSource 以支持使用 SQL 缓存依赖项Products进行缓存(Categories单击以查看全尺寸图像

将 ObjectDataSource 配置为支持缓存后,请通过浏览器重新访问页面。 同样,文本“选择事件触发”应在第一次访问页面时显示,但在分页、排序或单击“编辑”或“取消”按钮时应消失。 这是因为将数据加载到 ObjectDataSource 缓存中后,它将一直保留在该缓存中,直到 ProductsCategories 表被修改,或通过 GridView 更新数据。

在分页浏览网格并注意到缺少“选择事件触发文本”后,打开一个新的浏览器窗口,然后导航到“编辑、插入和删除”部分的“基础教程”(~/EditInsertDelete/Basics.aspx)。 更新产品的名称或价格。 然后,使用第一个浏览器窗口查看不同的数据页面、对网格进行排序或单击行的“编辑”按钮。 这一次,“选择事件触发”应重新显现,因为基础数据库的数据已被修改(见图10)。 如果文本未显示,请稍等片刻再试。 请记住,轮询服务每pollTime毫秒检查一次Products表的更改,因此在基础数据更新和缓存数据被逐出之间会有延迟。

修改产品表会逐出缓存的产品数据

图 10:修改产品表会逐出缓存的产品数据(单击以查看全尺寸图像

步骤 6:以编程方式使用SqlCacheDependency

体系结构教程中的缓存数据介绍了在体系结构中使用单独的缓存层的好处,而不是将缓存与 ObjectDataSource 紧密耦合。 在本教程中,我们创建了一个 ProductsCL 类,以编程方式演示如何使用数据缓存。 若要在缓存层中使用 SQL 缓存依赖项,请使用类 SqlCacheDependency

使用轮询系统时, SqlCacheDependency 对象必须与特定的数据库和表对相关联。 例如,以下代码基于 Northwind 数据库的Products表创建SqlCacheDependency对象:

Caching.SqlCacheDependency productsTableDependency = 
    new Caching.SqlCacheDependency("NorthwindDB", "Products");

构造函数的 SqlCacheDependency 两个输入参数分别是数据库和表名。 与 ObjectDataSource s SqlCacheDependency 属性一样,使用的数据库名称与元素属性Web.config中指定的name<add>值相同。 表名称是数据库表的实际名称。

要将某个 SqlCacheDependency 与添加到数据缓存的项目关联起来,请使用接受依赖项的 Insert 方法重载之一。 以下代码在无限期内向数据缓存添加,但将其与表上的ProductsSqlCacheDependency相关联。 简言之, 将保留在缓存中,直到由于内存约束被逐出,或者轮询系统检测到 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 类当前使用基于时间的60秒到期策略从 Products 表缓存数据。 让我们更新此类,使其改用 SQL 缓存依赖项。 负责 ProductsCL 将数据添加到缓存的类 s 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);
}

若要测试此功能,请将 GridView 添加到现有 ProductsDeclarative GridView 下方的页面。 将这个新的 GridView ID 设置为 ProductsProgrammatic,并通过它的智能标签将其绑定到一个名为 ProductsDataSourceProgrammatic 的新 ObjectDataSource。 将 ObjectDataSource 配置为使用 ProductsCL 类,并将 SELECT 和 UPDATE 选项卡中的下拉列表分别设置为 GetProductsUpdateProduct

将 ObjectDataSource 配置为使用 ProductsCL 类

图 11:将 ObjectDataSource 配置为使用 ProductsCL 类(单击以查看全尺寸图像

从 SELECT 选项卡 Drop-Down 列表中选择 GetProducts 方法

图 12GetProducts 从 SELECT 选项卡 Drop-Down 列表中选择方法(单击以查看全尺寸图像

从 UPDATE 选项卡 Drop-Down 列表中选择 UpdateProduct 方法

图 13:从 UPDATE 选项卡 Drop-Down 列表中选择 UpdateProduct 方法(单击以查看全尺寸图像

完成“配置数据源”向导后,Visual Studio 将在 GridView 中为每个数据字段创建 BoundFields 和 CheckBoxFields。 请像处理添加到此页面的第一个 GridView 一样,删除除ProductNameCategoryNameUnitPrice之外的所有字段,并根据需要设置这些字段的格式。 在 GridView 智能标记中,选中“启用分页”、“启用排序”和“启用编辑”复选框。 与 ProductsDataSourceDeclarative ObjectDataSource 一样,Visual Studio 会将 ObjectDataSource 属性OldValuesParameterFormatString设置为 ProductsDataSourceProgrammaticoriginal_{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 页面后,打开第二个浏览器窗口,导航到“编辑、插入和删除”部分的“基础”教程(~/EditInsertDelete/Basics.aspx)。 从“产品”表更新记录,然后在第一个浏览器窗口中查看新页面或单击其中一个排序标头。

在这种情况下,您将看到两件事情之一:断点被触发,说明由于数据库中的更改,缓存的数据被移除;或者断点未被触发,这意味着 SqlCacheDependencies.aspx 现在显示的是过时的数据。 如果断点未命中,则可能是由于数据发生了更改,而轮询机制尚未触发。 请记住,轮询服务每pollTime毫秒检查一次Products表的更改,因此在更新基础数据和缓存数据被逐出之间存在延迟。

注释

通过 GridView SqlCacheDependencies.aspx编辑其中一个产品时,此延迟更有可能出现。 在教程《架构中的数据缓存》中,我们添加了 MasterCacheKeyArray 缓存依赖项,以确保通过 ProductsCL 类的 UpdateProduct 方法编辑的数据已从缓存中逐出。 但是,在此步骤前面修改 AddCacheItem 方法时,我们替换了此缓存依赖项,因此类 ProductsCL 将继续显示缓存的数据,直到轮询系统记下对表的 Products 更改。 我们将了解如何在步骤 7 中重新引入 MasterCacheKeyArray 缓存依赖项。

步骤 7:将多个依赖项与缓存项相关联

回想一下, MasterCacheKeyArray 缓存依赖项用于确保在更新其中的任何单个项时从缓存中逐出 所有 与产品相关的数据。 例如, GetProductsByCategoryID(categoryID) 该方法缓存 ProductsDataTables 每个唯一 categoryID 值的实例。 如果逐出其中一个对象,则 MasterCacheKeyArray 缓存依赖项可确保也会删除其他对象。 如果没有此缓存依赖项,则当缓存的数据被修改时,可能存在其他缓存的产品数据过期的可能性。 因此,在使用 SQL 缓存依赖项时,请务必维护 MasterCacheKeyArray 缓存依赖项。 但是,数据缓存 Insert 的方法仅允许单个依赖项对象。

此外,在使用 SQL 缓存依赖项时,可能需要将多个数据库表关联为依赖项。 例如, ProductsDataTable 类中的 ProductsCL 缓存包含每个产品的类别和供应商名称,但 AddCacheItem 该方法仅使用依赖项 Products。 在这种情况下,如果用户更新类别或供应商的名称,则缓存的产品数据将保留在缓存中并过期。 因此,我们希望使缓存的产品数据不仅依赖于Products表,还依赖于Categories表和Suppliers表。

AggregateCacheDependency 提供了一种将多个依赖项与缓存项相关联的方法。 首先创建实例 AggregateCacheDependency 。 接下来,使用 AggregateCacheDependency s Add 方法添加依赖项集。 在随后将数据插入数据缓存中时,传入 AggregateCacheDependency 实例。 当任何AggregateCacheDependency实例的依赖项发生更改时,将逐出缓存的项。

下面显示了ProductsCL类的AddCacheItem方法的更新代码。 该方法创建MasterCacheKeyArray缓存依赖项,并为ProductsCategoriesSuppliers表创建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);
}

测试这段新代码。现在,对ProductsCategoriesSuppliers表的更改会导致缓存的数据被移除。 此外, ProductsCL 类 s UpdateProduct 方法(在通过 GridView 编辑产品时调用)将逐出 MasterCacheKeyArray 缓存依赖项,这会导致 ProductsDataTable 缓存被逐出,并在下一个请求上重新检索数据。

注释

SQL 缓存依赖项还可用于 输出缓存。 有关此功能的演示,请参阅: 对 SQL Server 使用 ASP.NET 输出缓存

概要

缓存数据库数据时,数据理想情况下会保留在缓存中,直到在数据库中修改数据。 使用 ASP.NET 2.0,可以在声明性和编程方案中创建和使用 SQL 缓存依赖项。 挑战之一在于发现数据何时被修改。 MICROSOFT SQL Server 2005 的完整版本提供通知功能,可在查询结果发生更改时向应用程序发出警报。 对于 SQL Server 2005 和更早版本的 SQL Server,必须改用轮询系统。 幸运的是,设置必要的轮询基础结构非常简单。

快乐编程!

深入阅读

有关本教程中讨论的主题的详细信息,请参阅以下资源:

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是《Sams 教你 24 小时学会 ASP.NET 2.0》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别致谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Marko Rangel、Teresa Murphy 和希尔顿 Giesenow。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com