【最佳实践】使用sharepoint对象模型编程时候的常见问题
原文地址:http://msdn.microsoft.com/en-us/library/bb687949.aspx
原文标题:
Best Practices: Common Coding Issues When Using the SharePoint Object Model
原文作者:
Scott Harris, Microsoft Corporation
Mike Ammerlaan, Microsoft Corporation
Steve Peschka, Microsoft Corporation
Roger Lamb, Microsoft Corporation
内容:
1、概述
2、高效的使用sharepoint数据和对象
3、操作文件夹和列表Folders和Lists
4、编写大量用户的应用
5、SPQuery查询对象的使用
6、Web Controls服务器端控件的使用
7、创建定时作业Timer Jobs
8、总结
9、附录
1、概述
使用sharepoint对象模型编码的开发者通常会碰到下列问题:性能、扩展性、可伸缩性。这篇文章给那
些遇到问题和需要提高现有sharepoint应用代码效率的程序员,或者你正在写新的sharepoint应用,都很有用。任何情况下,重点是理解如何高效的使用sharepoint对象模型,如何将缓存、多线程这类常用的编码技巧运用到sharepoint这个特殊的平台。这篇文章使你在正确的设计sharepoint应用,找到和修复代码中的错误,避免一些意想不到的错误方面变得更加容易。
下列领域反映出sharepoint开发者最常见的担忧
1)高效的使用sharepoint对象模型
2)文件夹、列表和SPQuery的性能问题
3)如何编写大量用户的应用
4)如何使用服务器端控件和Timer Jobs
5)Sharepoint对象的垃圾回收
这篇文章将讲述除去最后一个问题的其它问题,最后一个关于sharepoint对象的垃圾回收问题将在Best Practices: Using Disposable Windows SharePoint Services Objects.中看到。
另外,我们推荐你在使用sharepoint对象模型之前好好的阅读下列文章
· Server and Site Architecture: Object Model Overview
· Understanding the Administrative Object Model of Windows SharePoint Services 3.0
高效的使用sharepoint对象模型
缓存是一个提高系统性能的好办法。但是你一定要考虑缓存的益处,同时要考虑线程安全的需要。另外,你不应该在事件接收器中创建特定的sharepoint对象,因为这么做将导致由于过度的数据库调用,而产生的性能问题。
下列描述了这些常见的担心,以便你优化使用harepoint对象和数据的方式。
缓存数据和对象模型
很多的开发者使用.NET框架提供的缓存对象,例如:System.Web.Caching.Cache,来更好的使用内存,增加整体的系统性能。但是很多对象都不是线程安全的,缓存这些对象将导致应用错误和异常的错误。
注意:
这部分讨论的缓存技术和Custom Caching Overview.讨论的web内容管理中的缓存优化不是一回事。
缓存sharepoint对象不是线程安全的
你可能尝试通过缓存从查询对象返回的SPListItemCollection来提高性能和内存的使用。通常情况下,这是一个好主意;但是,SPListItemCollection包含一个内嵌的SPWeb对象,SPWeb对象不是线程安全的,不应该被缓存。
例如:假定SPListItemCollection对象被缓存在一个线程中。另外一个线程尝试去读取这个对象,这时候应用会失败或者表现奇怪,因为内嵌的SPWeb对象不是线程安全的;关于SPWeb对象和它的线程安全的更多信息,可以参考Microsoft.SharePoint.SPWeb
下面的部分将告诉你如何避免多线程读同一个缓存对象。
理解线程同步的潜在问题。
你可能没有意识到你的代码正运行在多线程的环境(默认的,Internet Information Services,或者说IIS,是多线程的),也没有意识到如何管理多线程环境。
下面的列子演示了一些程序员在代码中缓存Microsoft.SharePoint.SPListItemCollection对象。
不好的代码实践
缓存一个可能会多线程读取的对象
Public void CacheData()
{
SPListItemCollection oListItems;
oListItems=(SPListItemCollection)Cache[“ListItemCacheName”];
if(oListItems==null)
{
oListItems=DoQueryToReturnItems();
Cache.Add(“ListItemCacheName”,oListItems,…..);
}
}
尽管上面的Cache使用方法是正确的,因为ASP.NET缓存兑现是线程安全的,它引入了潜在的性能问题。(更多关于ASP.NET缓存的信息参照:Cache http://msdn.microsoft.com/en-us/library/system.web.caching.cache.aspx )如果前面的列子中的查询花费了10秒钟,在这10秒钟内,许多用户同时访问页面。在这个列子中,全部用户将会运行同样的查询语句,会更新同一个缓存对象。如果同样的查询运行10次,50次,100次,更多的线程在同时要更新同一个缓存对象,特别是在多核CPU、超线程计算机上,性能问题将变得更加严重。
为了避免多个查询同时访问相同的对象,你可以修改成下面的代码
使用锁,然后检查是否为空
Private static object _lock=new object();
Public void CacheData()
{
SPListItemCollection oListItems;
Lock(_lock)
{
oListItems=(SPListItemCollection)Cache[“ListItemCacheName”];
If(oListItems==null)
{
oListItems=DoQueryToReturnItems();
Cache.Add(“ListItemCacheName”,oListItems,…..);
}
}
}
你也可以通过在If(oListItems==null)的代码块中添加锁,来稍微的提升性能。在你做这些的时候,不需要挂起全部的线程来检查对象是否被缓存。依赖于执行查询,返回结果的时间,还是有可能超过一个用户在同时执行了这个查询。如果你是用多核CPU,这是很有可能发生的。请记住,CPU越多,执行查询的时间越长。将锁放在if()代码块中将会发生这些。如果你想完全的保证在当前线程正在创建的时候,另外一个线程不会创建,你可以使用下面的代码。
使用锁,再次检查是否为空
Private static object _lock=new object();
Public void CacheData()
{
SPListItemCollection oListItems;
oListItems=(SPListItemCollection)Cache[“ListItemCacheName”];
If(oListItems==null)
{
Lock(_lock)
{
If(oListItems==null)
{
oListItems=(SPListItemCollection)Cache[“ListItemCacheName”];
{
oListItems=DoQueryToReturnItems();
Cache.Add(“ListItemCacheName”,oListItems,…..);
}
}
}
}
}
如果缓存已经被填充,最后的例子和最开始的实现在性能上是一样的。如果缓存没有被填充,同时系统负载较轻的情况下,使用锁将导致轻微的性能损失。系统在负载较重的情况下,这个办法应该极大的提高了系统的性能,因为查询会被执行一次,而不是多次执行。和同步的花费相比较,查询通常会更加花费时间。
上买的代码在IIS运行的关键部位挂起了其他的全部线程,阻止其他线程访问对象,直到对象完成。这样解决了线程同步问题,但是,代码仍然不是正确的,因为它正在缓存一个非线程安全的对象。
为了解决线程安全,你可以缓存一个从SPListItemCollection创建的DataTable对象。你可以像下面这样修改代码,从DataTable中获取数据。
好的代码实践,缓存一个DataTable对象
Private static object _lock =new object();
Public void CacheData()
{
DataTable oDataTable;
SPListItemCollection oListItems;
Lock(_lock)
{
oDataTable=(DataTable)Cache[“ListItemCacheName”];
if(oDataTable==null)
{
oListItems=DoQueryToReturnItems();
oDataTable=oListItems.GetDataTable();
Cache.Add(“ListItemCacheName”,oDataTable);
}
}
}
更多关于DataTabel对象使用的例子,和其他开发sharepoint应用的好主意,请查看DataTable的相关主题。
在事件接收器中使用对象
不要在事件接收器中实例化SPWeb、SPSite、SPList或者是SPListItem对象。在事件接收器中实例化这些对象,而不是properties的属性来获取这些对象的方法会导致下列的问题:
1) 将引起额外的数据库循环错误,在一个事件接收器中,一个写的操作最多能导致额外的5个循环处理。
2) 调用这些对象的Update方法将导致在其他注册的事件接收器中的update方法调用失败。
不好的代码实践,在事件接收器中实例化SPSite对象
Public override void ItemDeleting(SPItemEventProperties properties)
{
Using(SPSite site=new SPSite(properties.WebUrl))
{
Using(SPWeb web=site.OpenWeb())
{
SPList list=web.Lists[properties.ListId];
SPListItem item=list.GetItemByUniqueId(properties.ListItemId);
}
}
}
好的代码实践,使用SPItemEventProperties
SPWeb web=properties.OpenWeb();
SPListItem item=properties.ListItem;
如果在你的代码中不这样的话,当你在新实例上用update的时候,你一定要在基类是SPEventPropertiesBase的恰当的子类中使用Invalidate方法使他失效,例如:SPItemEventProperties.InvalidateListItem or SPItemEventProperties.InvalidateWeb
操作文件夹和列表
当文件夹和列表越来越到的时候,上面运行的自定义代码需要被调整为最优。否则,你的应用可能会运行的很慢,甚至发生超时。下面的这些建议是针对大量文件夹和列表导致的性能问题设计的。
1)不使用SPList.Items
SPList.Iitems是从文件夹中获取所有的条目,包括了列表中全部的列。应该使用下列代替方案。
获取列表中的全部列
使用SPList.GetItems(SPQuery query)来代替SPList.Items,使用过滤器,只获取你想要得到的列可以提高效率。如果列表包含2000条以上的记录,你就需要分页,下面的代码展示了如何进行分页。
好的代码实践,使用SPList.GetItems获取数据
SPQuery query=new SPQuery();
SPListItemCollection spListItems;
String lastItemIdOnPage=null;
Int itemCount=2000;
While(itemCount==2000)
{
Query.ViewFields=”<FieldRef Name=\”ID\” /><FieldRef Name=\”Title\” />”;
Query.RowLimit=2000;
Query.ViewAttributes=”Scope=\”Recursive\””;
StringBuilder sb=new StringBuilder();
Sb.Append(“<OrderBy Override=\”true\” ><FieldRef Name=\”ID\” /></OrderBy>”);
Query.query=sb.ToString();
SPListItemCollectionPosition pos=new SPListItemCollectionPosition(lastItemOnPage);
Query.ListItemCollecitonPosition=pos;
spListItems=spList.GetItems(query);
lastItemOnPage=spListItems.ListItemCollectionPosition.PageInfo;
itemCount=spListItems.Count;
}
下面的代码展示了如何列举和分页大列表
· SPWeb oWebsite = SPContext.Current.Web;
· SPList oList = oWebsite.Lists["Announcements"];
·
· SPQuery oQuery = new SPQuery();
· oQuery.RowLimit = 10;
· int intIndex = 1;
·
· do
· {
· Response.Write("<BR>Page: " + intIndex + "<BR>");
· SPListItemCollection collListItems = oList.GetItems(oQuery);
·
· foreach (SPListItem oListItem in collListItems)
· {
· Response.Write(SPEncode.HtmlEncode(oListItem["Title"].ToString()) +"<BR>");
· }
·
· oQuery.ListItemCollectionPosition = collListItems.ListItemCollectionPosition;
· intIndex++;
} while (oQuery.ListItemCollectionPosition != null);
通过标识来获取items
使用SPWeb.GetItemById(int id, string field1, params string[] fields).代替SPList.Items.GetItemById获取数据,指明你想要的字段。
2、 不要枚举全部的SPList.Items集合或者是SPFolder.Files集合
使用下表左侧的方法将枚举全部的SPList.Items集合,在大列表中将导致性能下降,相反,应该使用下表右侧的方法。
Poor Performing Methods and Properties |
Better Performing Alternatives |
SPList.Items.Count |
SPList.ItemCount |
SPList.Items.XmlDataSchema |
Create an SPQuery object to retrieve only the items you want. |
SPList.Items.NumberOfFields |
Create an SPQuery object (specifying the ViewFields) to retrieve only the items you want. |
SPList.Items[System.Guid] |
SPList.GetItemByUniqueId(System.Guid) |
SPList.Items[System.Int32] |
SPList.GetItemById(System.Int32) |
SPList.Items.GetItemById(System.Int32) |
SPList.GetItemById(System.Int32) |
SPList.Items.ReorderItems(System.Boolean[],System.Int32[],System.Int32) |
Perform a paged query by using SPQuery and reorder the items within each page. |
SPFolder.Files.Count |
SPFolder.ItemCount |
注意:
SPList.ItemCount属性是推荐的获取列表条目数量的方式,使用这个属性可以提高性能,但是也可能会返回以外的结果。如果要求精确的数量,可以使用GetItems(SPQuery query)来获取。
3、 PortalSiteMapProvider的使用
在Steve Peschka's的白皮书Working with Large Lists in Office SharePoint Server 2007中讲述了一种在MOSS2007中高效的获取列表数据的方法,就是使用 PortalSiteMapProvider类。它会自动的为获取的列表数据提供缓存机制。其中的GetCachedListItemsByQuery方法,以SPQuery对象为一个参数,会查看items是否已经缓存,如果缓存,则返回缓存的值。如果没有,它会查询列表的值,然后缓存起来。这个方法适合于数据不经常变化的情况,如果数据集经常变化,就会有性能损失,因为除了额外的从数据库读取数据,还要写缓存。它使用站点集对象存储数据,这个缓存值默认是100MB,你可以为每个站点集设置这个值。但是它占用的是应用池共享的内存,因此可能影响应用池中的其他应用。另一个重要限制是你不能在WinForm应用中使用这个类。下面的代码显示了如何使用这个类
1. // Get the current SPWeb object.
2. SPWeb curWeb = SPControl.GetContextWeb(HttpContext.Current);
3.
4. // Create the query.
5. SPQuery curQry = new SPQuery();
6. curQry.Query = "<Where><Eq><FieldRef Name='Expense_x0020_Category'/>
7. <Value Type='Text'>Hotel</Value></Eq></Where>";
8.
9. // Create an instance of PortalSiteMapProvider.
10. PortalSiteMapProvider ps = PortalSiteMapProvider.WebSiteMapProvider;
11. PortalWebSiteMapNode pNode = ps.FindSiteMapNode(curWeb.ServerRelativeUrl) as PortalWebSiteMapNode;
12.
13. // Retrieve the items.
14.
15. SiteMapNodeCollection pItems = ps.GetCachedListItemsByQuery(pNode, "myListName_NotID", curQry, curWeb);
16.
17. // Enumerate through all of the matches.
18. foreach (PortalListItemSiteMapNode pItem in pItems)
19. {
20. // Do something with each match.
}
4、在任何可能的情况下,使用列表的GUID或者是url来获取对列的引用
你可以通过SPWeb.Lists属性获取SPList,使用列表的GUID或者是标题作为索引。使用SPWeb.Lists[GUID]和SPWeb.Lists[strURL]的性能要优于SPWeb.Lists[strDisplayName]。使用GUID是最好的,因为GUID唯一,永久,只需要一个单一的数据库查询。使用标题作为索引需要获取全部的列表标题,然后进行比较。如果你有列表的url,没有GUID,你可以在获取列表之前使用GetList方法在内容数据库中来查询列表的GUID。
写一个有很多用户的应用
你可能还没有意识到你应该写具有可升级性的代码,以便它可以处理大并发用户。一个好的例子是为所有的站点和子站点的页面创建自定义的导航。如果你的公司在内网有一个sharepoint站点,每个部门又有自己的站点和许多子站点,你的代码就应该向下面一样:
public void GetNavigationInfoForAllSitesAndWebs()
{
foreach(SPSite oSPSite in SPContext.Current.Site.WebApplication.Sites)
{
try
{
SPWeb oSPWeb = oSPSite.RootWeb;
AddAllWebs(oSPWeb );
}
finally
{
oSPSite.Dispose();
}
}
}
public void AddAllWebs(SPWeb oSPWeb)
{
foreach(SPWeb oSubWeb in oSPWeb.Webs)
{
try
{
//.. Code to add items ..
AddAllWebs(oSubWeb);
}
finally
{
if (oSubWeb != null)
oSubWeb.Dispose();
}
}
}
上面的代码正确的回收所有的对象,它仍然出了问题,因为代码一遍又一遍的举了同一个列表。例如:你有10个站点,平均每个站点有20个子站点,你将会枚举他们200次。用户比较少的时候,不会出现什么性能问题。但是,随着你往系统中添加更多的用户,问题变得越来越严重,从下面的表中可以看出来。
Users |
Iterations |
10 |
2000 |
50 |
10000 |
100 |
200000 |
250 |
500000 |
尽管代码是为每个点击系统的用户执行,但是每个用户的数据是一样的。这时候性能的影响依赖于代码做了些什么。在某些情况,重复的代码可能不会引起什么性能问题;但是,在上面的这个例子中,系统一定会创建一个COM对象(在从他们的集合中获取的时候SPSite或者是SPWeb对象会被创建出来),从对象获取数据,然后回收集合中的每一个对象。这会对性能有重大的影响。
如何使代码在一个多用户的环境更有可伸缩性呢,或者说调整的更好呢,这是一个很难回答的问题。它依赖于应用设计的目的。
当需要代码更具有可伸缩性的时候,你应该从以下几个方面考虑:
1)数据是否从来没有改变,或者说很少变动,偶尔变动,还是经常性的变化?
2)数据对全体用户都是一样的吗,会变化吗?例如,数据依赖于登录的用户,某些被访问的部分,还是随着年或者季节的变化而变化呢?
3)数据是很容易访问还是需要长时间来获取呢?例如,数据是从一个长期运行的数据库查询获取呢,还是从一个远程数据库定时获取呢?
4)数据是公开的,还是需要更高级别的安全?
5)数据的大小
6)Sharepoint站点是在一个单服务器还是在一个服务器场。
上面这些问题的答案,将会决定你是用什么方法使得你的代码更有可伸缩性,可以处理大量用户。这篇文章的目的不是为所有的问题和解决方案提供答案,只是提供一些小的建议,这些建议你可以应用到你的特殊需求中。
缓存行数据,Caching Row Data
你可以是用System.Web.Caching.Cache对象来缓存数据。这个对象要求你查询一次数据,然后存入缓存中以便其他用户访问。
如果你的数据是静态不变的,你可以只建立一次缓存,永不过期,直到应用重启,或者是每天加载一次数据,爆出数据更新。你可以在应用启动的时候创建缓存,也可以在第一个用户session开始,或者第一个用户试图访问数据的时候创建缓存。
如果你的数据变化的少,基本是静态的,你可以设置一个过期时间,几秒,几分钟,或者是几小时更新一次。这使得你的数据在一个用户可以接收的时间范围内更新。尽管数据被缓存30秒,在负载很重的情况下,你还是会看到性能的提升,因为你的代码每30密爱只运行一次,而不是点击系统的每个用户每秒运行一次。
无论你什么时候更新数据,安全限制都是另外一个问题。例如,如果你缓存了通过枚举列表获取的items,你可能只是获取了一些数据,数据只能当前用户查看,或者你是用DataTable对象缓存列表中的全部条目,你就不能很容易的使用安全限制属于哪个组的用户能看一部分数据。更多的关于在缓存中存储安全限制的数据,你可以查看CrossListQueryCache。
另外,你一定要考虑Caching Data and Objects.中描述的问题。
在显示之前创建好数据
想一下,你是如何使用你缓存的数据的。如果这些数据在运行时使用,将它们放入DataSet或者DataTable缓存起来可能是一个好主意。你可以在运行的时候查询这些数据,如果这些数据被用来展示成一个列表,表格,格式化的网页,可以考虑创建一个显示的对象,并缓存这个对象。在运行的时候,你只需要从缓存中获取对象,然后调用它的render方法就可以显示内容了。你也可以缓存render好的结果,但是,这样会导致安全问题,同时缓存的内容会非常大,导致增加了页面的交换内存。
为单服务缓存,还是服务器场缓存
依赖于你如何建立你的sharepoint网站集,你可能要面对一些缓存问题。如果你的数据在所有的服务器,任何时候都是一样的,你要确保在每台服务器缓存相同的数据。
一个可以确保数据被缓存的方法,就是将它缓存在一台服务器或者sql server 的数据库中。同时,你要考虑数据访问缓存在特定服务器上的数据的时间和安全性。
你也可以创建一个业务层对象,然后缓存在一台特定的服务器上,然后通过各种API来访问数据。
查询对象的使用
SPQuery查询对象在任何返回大数据量的情况下,都会产生性能问题。下面的建议将会帮助你优化你的代码,以便在你查询大量数据的时候性能不至于过大的损失。
1)不要使用没有边界的SPQuery对象,没有RowLimit限制的查询对象在大数据量的列表中性能会很低甚至失败。设置1-2000,如果有必要,就分页。
2)使用索引列,如果你查询一个没有索引的列,查询将会被阻塞,无论什么时候它将扫描比query临界值更多的项。给RowLimit设置一个比临界值小的值。
3)
如果你知道你的列表项的url和你想要的列的话,可以使用use SPWeb.GetListItem(string strUrl, string field1, params string[] fields)
可是这个方法我一直都没有找到啊,在SPWeb下面就只有GetListItem,不过参数就是url,没有后面的,小郁闷了一把。
使用服务器端控件
当你继承或者重写
Microsoft.SharePoint.WebControls命名空间下面的控件的时候,记住sharepoint的服务器端控件时模版控件。不像ASP.NET的服务器端控件,sharepoint的服务器端控件使用模版定义和呈现,而不是CreateChildControls方法。不像使用CreateChildControls方法创建控件,sharepoint服务器段控件使用控件的Template, AlternateTemplate, DisplayTemplate, CustomTemplate, 和AlternateCustomTemplate属性呈现控件。
创建定时作业Timer Jobs
总结
为了确保你的sharepoint应用的最好性能,你需要回答下面的问题:
1) 我的代码是否正确的释放了sharepoint对象。
2) 我的代码是否正确的缓存了对象。
3) 我的代码是否缓存了正确的类型
4) 我的代码在必要的时候是否使用了线程同步。
5) 我的代码在1000个用户的时候是否也像10个用户的时候一样的运行良好
如果在写代码的时候你考虑了这些问题,你的sharepoint应用将会运行的更高效,你的用户将有更好的体验。你也能在你的系统中避免意外和错误的发生。