在ASP.NET 2.0中操作数据之五十六:使用ObjectDataSource缓存数据
作者:heker2007
导言
就计算机科学而言,caching就是将所需要的数据或信息的备份放在某个地方,便于快速访问的这样一个过程。以数据处理(data-driven)程序为例,程序的大部分时间浪费在数据查询上。要提升这种程序的性能,通常的做法是将查询结果存放在程序的存储器里。
ASP.NET 2.0提供了各种各样的缓存方式。对web页面和用户控件可以通过output caching进行缓存;同样我们可以通过ObjectDataSource 和SqlDataSource控件,在控件级(control level)对数据进行缓存;同时,ASP.NET的data cache提供了丰富的缓存接口(caching API),供页面开发员通过编程缓存对象。在本文及接下来的3篇文章我们将对ObjectDataSource的缓存属性以及data cache进行考察;我们也将探究如何在启动时对application-wide数据进行缓存,以及通过使用SQL cache dependencies对缓存数据刷新。
主要的缓存要点
由于缓存通过将数据的副本放置在一个便于快速访问的地方来提高程序的总体性能。由于它仅仅是一个副本,当源数据发生改变时,副本不能同步更新。为此,页面开发员应制定一个标准将其清除出内存,可以使用如下的2种方法之一:
Time-based标准:向内存添加的条目(item),只能在内存里驻留固定或灵活(sliding)的一段时间。比如,开发者可设定一个时间段,比如60秒,当条目添加到内存后,不管访问它的频率有多高,60秒后就会被清除掉;如果是灵活(sliding)处理的话,当最后一次被访问后,未再次被访问的时间一旦超出60秒,也会被清除掉。
Dependency-based标准:当向内存添加条目时为其分配一个从属体(dependency),当条目对应的从属体发生改变时将条目清除掉。从属体可以是一个文件;另一个缓存条目;或者干脆是这两者的混合体( combination);当然还可以是SQL cache dependencies,它可以向内存添加条目,当源数据改变时将条目清除掉。我们将在接下来的文章《使用SQL缓存依赖项SqlCacheDependency 》里详细考察。
不管是哪种标准,在条目被清除掉以前,我们都可以对其访问。如果内存达到了它的极限,它会清除掉已有的条目后再添加新的条目。因此,当处理缓存数据时很重要的一点是我们要充分考虑到缓存数据已被清除的可能。在下一篇文章《在分层架构中缓存数据》我们考察采用哪种模式从内存访问数据。
缓存是提升程序性能的一种较为经济的方法,就像Steven Smith在他的文章《ASP.NET Caching: Techniques and Best Practices:》里阐述的一样:“缓存是获得‘上佳'性能的一种好方法,不需要太多的时间和分析。… 存储器也便宜,要获得你期望的性能,靠缓存技术你需要花30秒;靠优化代码和数据库你可能要几天乃至几周时间…”
虽然缓存可以显而易见的提升系统性能,但并不是适用于所有的应用程序,比如某些实时(real-time)、频繁更新数据的程序就不适合。
但是对大部分程序而言,还是适用的。关于ASP.NET 2.0里的缓存的更多背景资料请参考ASP.NET 2.0 QuickStart Tutorials系列的Caching for Performance 部分。
第一步:创建Caching页面
在我们开始以前,首先让我们花些时间来添加包括本篇在内的最近四篇教程需要用到的页面。我们先在项目中新建一个称作Caching的文件夹,接下来,为目录新增以下几个页面,并配置为使用Site.master母板页。
Default.aspx
ObjectDataSource.aspx
FromTheArchitecture.aspx
AtApplicationStartup.aspx
SqlCacheDependencies.aspx
图1:创建相关的ASP.NET页面
像其它文件夹一样,Caching文件夹里的Default.aspx页面将本系列的文章显示出来。记得用户控件SectionLevelTutorialListing.ascx提供该功能,设计模式里将其拖到页面上。
图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文件的更新后,让我们在浏览器里查看,左边的菜单栏显示caching章节的文章
图3:网站地图Site Map包含了Caching章节的文章
第二步:在Web Page页面里展示产品
本文考察怎样使用ObjectDataSource控件内置(built-in)的缓存功能。在开始之前,我们首先需要创建一个页面,用一个ObjectDataSource控件调用ProductsBLL class类获取产品信息,再用GridView控件展示出来。
首先打开Caching文件夹里的ObjectDataSource.aspx页面。从工具箱拖一个GridView控件到页面,设置其ID为Products,再从智能标签里选择将其绑定到一个ObjectDataSource控件,ID为ProductsDataSource。设该ObjectDataSource使用ProductsBLL class类。
图4:设置ObjectDataSource控件使用ProductsBLL Class类
在本页面,我们要创建一个允许编辑的GridView控件,当ObjectDataSource控件里的缓存数据发生改变时,我们可以通过GridView的界面查看到底会发生什么。在SELECT标签里选择默认的GetProducts()方法, 但是在UPDATE标签里选择接受productName, unitPrice 和productID作为输入参数的UpdateProduct()重载方法。
图5:在UPDATE标签里选择重载的UpdateProduct()方法
最后,在INSERT和DELETE标签里选择“(None)”,点完成按钮。一旦完成“设置数据源向导”,Visual Studio会将ObjectDataSource控件的OldValuesParameterFormatString属性设置为original_{0}。就像在前面的教程之16章《概述插入、更新和删除数据》里探讨的一样,该属性要么删除掉,要么设置为{0},不然的话更新操作会报错。
此外,完成向导后,Visual Studio会将产品的所有数据列添加到GridView控件,将除了ProductName, CategoryName和UnitPrice之外的所有绑定列(BoundFields)删除。然后,分别将上述3列的HeaderText属性改为Product”, “Category”和“Price”。由于ProductName是必需的,将ProductName列转变成模板列(TemplateField),在EditItemTemplate里添加一个RequiredFieldValidator控件;同样的,将UnitPrice列也转换成模板列,并添加一个CompareValidator控件,确保用户输入的是大于或等于0的有效的货币值。除此以外,你还可以作一些界面上的改进,比如使UnitPrice值居中,或分别对UnitPrice的只读和编辑界面作一些格式化的处理。
在GridView的智能标签里点相关项启动编辑、分页、排序功能。
注意:想回顾怎样自定义GridView的编辑界面吗?请参考前面的文章之20《定制数据修改界面》
图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列出了每个产品的name, category和price信息。花几分钟测试页面—对结果排序,查看分页,编辑某条记录。
图7:显示每条记录的Name, Category和Price信息
第三步:考察ObjectDataSource如何请求数据
ID为Products的GridView通过调用名为ProductsDataSource的ObjectDataSource的Select()方法检索数据并将它显示出来。该ObjectDataSource创建业务逻辑层的ProductsBLL class类的一个实例并调用它的GetProducts()方法,该方法又调用数据访问层ProductsTableAdapter的GetProducts()方法。数据访问层连接到数据库Northwind,并执行已设置好了的SELECT查询。查询数据以NorthwindDataTable的形式返回到数据访问层,该DataTable对象再依次传回到业务逻辑层,ObjectDataSource、GridView控件。GridView控件为DataTable里的每一数据行(DataRow)创建一个GridViewRow对象,每个GridViewRow对象最终被编译为HTML返回到客户端,呈现在访问者的浏览器里。
任何时候,当GridView控件需要绑定时,按上述的事件发生顺序执行。比如,首次登录页面;将数据从一个页面传递到另一个页面;在GridView里排序;通过GridView内建的编辑或删除界面改动数据。当GridView的视图(view sta被设为disabled时,每次页面回传时也会对GridView重新绑定;当然我们可以显式地调用DataBind()方法来对GridView实施绑定。
为了更清除地揭示从数据库检索数据的频率,我们显示一个消息,提示在某时程序在检索数据。为此,在GridView控件上添加一个ID为ODSEvents的Label控件,清除其Text属性,将其EnableViewState属性设置为false。在Label控件下面再添加一个Button控件,设其Text属性为“Postback”.
图8:在GridView上添加Label 和 Button控件
在整个数据检索过程中,首先触发ObjectDataSource的Selecting事件,并调用其对应的已设置好的方法。为该事件创建一个事件处理器,添加如下的代码:
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通过它的视图状态(view state)载入数据进行重建(reconstructed),因此不再需要通过ObjectDataSource检索数据库来得到数据进行重建。然而,排序、分页、编辑等都会促使GridView重新绑定到数据源,因此,文本“Selecting event fired”又出现了。
图9:当GridView重新绑定到数据源时,显示文本“Selecting event fired”
图10:点“Postback” 按钮导致GridView从视图状态“View State”获取数据
每次分页、排序时都需要从数据库检索数据,这看起来有点浪费资源。即便GridView不支持排序和分页,任何人每次第一次登录页面时都需要从数据库检索数据(如果将view state设置为disabled的话,每次页面回转也会检索数据)。如果GridView对所有用户显示的数据都是一样话,那么额外的数据库查询是浪费。我们可以对GetProducts()方法返回的数据进行缓存,再将GridView绑定到这些缓存数据。
第四步:用ObjectDataSource缓存数据
仅仅简单的设置某些属性,我们就可以让ObjectDataSource对它的检索数据自动的进行缓存。以下总结了ObjectDataSource控件的与缓存相关的属性:
EnableCaching—必须设置为true,默认为false.
CacheDuration—缓存时间,以秒为单位。默认为0,只有当EnableCaching属性设置为true,且CacheDuration设为大于0的值时ObjectDataSource控件才会缓存数据。
CacheExpirationPolicy—可设置为Absolute 或 Sliding。如果为Absolute,当它设为多少秒时,ObjectDataSource就会对检索的数据缓存多少秒;如果为Sliding,当它设为多少秒时,一旦超过那么多秒没有对缓存数据进行访问,就终止缓存。默认为Absolute。
CacheKeyDependency—用该属性将ObjectDataSource的缓存条目(entry)与现有的缓存从属体关联起来。利用可以它将缓存条目提前从内存清除掉。绝大多数情况下用该属性把SQL cache dependency与ObjectDataSource的缓存关联起来。这个话题我们将在后面的教程《使用SQL缓存依赖项SqlCacheDependency 》考察。
让我们设置ID为ProductsDataSource的ObjectDataSource 的数据缓存时间为30秒。设其EnableCaching属性为true;设其CacheDuration属性为30;CacheExpirationPolicy属性为默认的Absolute。
图11:设置ObjectDataSource的缓存时间为30秒
保存你的设置,并在浏览器里查看。当你第一次登录页面时,文本“Selecting event fired”会显示出来,因为原始数据还未缓存。但你点“Postback”按钮,或进行分页,排序,或点编辑、取消按钮时,文本“Selecting event fired”就不会显示出来了。原因是只有当ObjectDataSource控件检索数据时才会触发Selecting事件;如果ObjectDataSource控件是从缓存里面获取数据的话就不会触发Selecting事件。
过了30秒后,数据将从内存清除;或者调用ObjectDataSource控件的Insert, Update,或Delete方法的话数据也会被清除掉。因此,过了30秒后或点击“Update”按钮,编辑,取消按钮,或排序、分页的话就会促使ObjectDataSource检索数据,触发Selecting事件,文本“Selecting event fired”又会显示出来。最后,再对检索得到的数据进行缓存。
注意:如果你看到文本“Selecting event fired”频繁的出现,很可能是内存容量太小。如果没有足够的容量,ObjectDataSource添加到内存的数据可能被清除掉了。如果ObjectDataSource没有或者只是偶尔地对数据缓存,请关闭一些应用程序来释放掉内存,然后再试一次。
图12揭示了ObjectDataSource的缓存流程。当文本“Selecting event fired”出现在屏幕上时,那是因为数据没有在缓存里找到,必须进行相关检索。当文本消失时,那是因为数据进行了缓存。当从缓存得到了所需的数据时,没有任何数据查询执行。
图12:ObjectDataSource在Data Cache里存储和获取数据
每一个ASP.NET应用程序有它自己的数据缓存实例,所有的页面和用户都可以进行访问。那意味着对于ObjectDataSource控件缓存的数据,所有登录该页面的用户都可以访问。来做个验证,在一个浏览器里打开ObjectDataSource.aspx页面,当第一次登录该页面时,文本“Selecting event fired”显现出来(假定前面测试时缓存的数据到此时已经被清除掉了)。再开第二个浏览器,将第一个浏览器里的URL地址拷贝、粘贴过来。在第二个浏览器里,文本“Selecting event fired”并没有显示出来,因为它使用的是第一个浏览器页面缓存的数据。
当向内存添加检索数据时,ObjectDataSource要用到一个叫cache key的值,该值包括:CacheDuration 和 CacheExpirationPolicy属性的值;ObjectDataSource调用的业务对象的类型(type),它由TypeName 属性指定(比如:ProductsBLL);SelectMethod 属性的值,以及SelectParameters参数集里参数的name 和 values;StartRowIndex 和 MaximumRows属性的值,它用来执行用户自定义分页(custom paging)。
将这些属性值组合在一起构成cache key值是为了对每一个缓存条目提供唯一的标识值。比如,在前面的教程里,我们使用ProductsBLL类的GetProductsByCategoryID(categoryID)方法来获取某个指定类的所有产品。假如一个用户在页面查看饮料类(其CategoryID值为1)的产品信息,如果ObjectDataSource控件在进行数据缓存时忽略SelectParameters的值,当另一个用户登录页面查看调味品类的产品信息时,恰好饮料类产品信息正好缓存在内存里,第二个用户将会看到饮料类的产品信息,而非他想要的调味品类的产品信息。所以,当cache key值包含electParameters的值的话,ObjectDataSource缓存数据的时候就可以将调味品类和饮料类区分开来。
数据更新不同步(Stale Data)问题
当调用ObjectDataSource控件的Insert, Update和 Delete其中一个方法时,它都会将缓存条目从内存清除掉。这样做的好处在于当从页面修改数据时将缓存的旧数据清除掉。然而,ObjectDataSource还是可能有将“未更新数据”(也就是源数据已经发生改变,而缓存的数据没同步更新)显示出来的情况。最简单的例子是直接从数据库修改数据,比如某个数据库管理员运行一个脚本,修改数据库里的某些记录。
在此,我们探讨一种微妙的情况。虽然调用ObjectDataSource的数据修改方法时它会将缓存数据清除掉,但清除的是那些与ObjectDataSource的“属性组合值”(combination of property)相匹配的缓存条目(比如CacheDuration, TypeName, SelectMethod等)。假如你有2个ObjectDataSources控件,它们更新相同的数据,当使用不同的SelectMethods 或 SelectParameters,当第一个ObjectDataSources控件更新某一行记录而清除该行对应的缓存数据时,第二个ObjectDataSources控件仍然使用该行对应的缓存数据。我们来做个验证,创建一个页面,包含可编辑的GridView控件,其对应的ObjectDataSource控件设置为使用缓存,且调用ProductsBLL类的GetProducts()方法来获取数据。在本页(或另外创建一个页面)再添加GridView 和ObjectDataSource控件,但是设置第二个ObjectDataSource控件调用GetProductsByCategoryID(categoryID)方法。由于这2个ObjectDataSource控件的SelectMethod属性不同,因此它们各自有各自不同的缓存值。如果你在第一个GridView控件里编辑某个产品,然后在第二个GridView控件里重新绑定数据(比如分页、排序等),我们在第二个GridView控件里看到该产品的值依然是“老的缓存数据”(而并不是第一个GridView控件修改后的值)
简而言之,如果你乐于使用“老的缓存数据”,那只有使用基于时间的缓存时间值(time-based expiries,也就是设置具体的缓存时间长度),如果对数据及时更新要求很高的话,将缓存时间设短点。如果不允许使用“老的缓存数据”的话,要么放弃缓存,要么使用SQL cache dependencies(你可以认为它是你缓存的数据库数据)。我们将在后面探讨SQL cache dependencies。
总结:
在本文我们考察了ObjectDataSource内建的缓存功能。仅仅设置很少的属性,我们可以将ObjectDataSource调用SelectMethod方法得到的数据进行缓存。其CacheDuration 和 CacheExpirationPolicy属性指定了缓存的时间和类型(absolute或sliding)。而CacheKeyDependency属性将ObjectDataSource的缓存实体与现有的缓存从属体(cache dependency)关联起来,一般是SQL cache dependencies。
因为ObjectDataSource只是简单地进行数据缓存,我们可以通过编程实现ObjectDataSource内建的这种功能。不过在表现层这样做没有意义,因为ObjectDataSource控件提供了该功能。不过我们可以在体系结构的其它层次实现缓存功能。为此,我们需要一个逻辑,它与ObjectDataSource调用的相同。在下一篇文章我们将考察如何在体系结构内部编程处理数据缓存。
祝编程快乐!
作者简介
本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的书,是4GuysFromRolla.com的创始人,自1998年以来一直应用 微软Web技术。大家可以点击查看全部教程《[翻译]Scott Mitchell 的ASP.NET 2.0数据教程》,希望对大家的学习ASP.NET有所帮助。
您可能感兴趣的文章:
- 在ASP.NET 2.0中操作数据之五十七:在分层架构中缓存数据
- 在ASP.NET 2.0中操作数据之五十八:在程序启动阶段缓存数据
- 在ASP.NET 2.0中操作数据之五十九:使用SQL缓存依赖项SqlCacheDependency
- 在ASP.NET 2.0中操作数据之六十:创建一个自定义的Database-Driven Site Map Provider
- 在ASP.NET 2.0中操作数据之六十一:在事务里对数据库修改进行封装
- 在ASP.NET 2.0中操作数据之六十二:GridView批量更新数据
- 在ASP.NET 2.0中操作数据之六十三:GridView实现批量删除数据
- 在ASP.NET 2.0中操作数据之六十四:GridView批量添加数据
- 在ASP.NET 2.0中操作数据之六十五:在TableAdapters中创建新的存储过程
- 在ASP.NET 2.0中操作数据之六十六:在TableAdapters中使用现有的存储过程