代码改变世界

微软文档翻译:【最佳实践】使用sharepoint对象模型编程时候的常见问题

2009-11-04 22:33  Virus-BeautyCode  阅读(1249)  评论(0编辑  收藏  举报

 

【最佳实践】使用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、操作文件夹和列表FoldersLists

4、编写大量用户的应用

5SPQuery查询对象的使用

6Web 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的相关主题。

      

在事件接收器中使用对象

       不要在事件接收器中实例化SPWebSPSiteSPList或者是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属性是推荐的获取列表条目数量的方式,使用这个属性可以提高性能,但是也可能会返回以外的结果。如果要求精确的数量,可以使用GetItemsSPQuery 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应用将会运行的更高效,你的用户将有更好的体验。你也能在你的系统中避免意外和错误的发生。