Linq to SharePoint,看上去很美


Linq to SharePoint是SharePoint 2010引入的一組新API,在這之前,如果我們想要按照條件過濾SharePoint列表中的數據,只能通過CAML。

但使用CAML並不是件令人身心愉悅的事情,至少我是這么認為的。我覺得在代碼中嵌入一塊冗長的XML字符串非常破壞美感,我尤其喜歡強類型,所以一直很難接受SPListItem用字符串作為鍵值去獲取Field值的方式,更別提這些值都是Object類型,還得再經過一次轉換。

所以我比較喜歡將SPListItem轉換成實體類來使用,只不過一直以來的做法都是自己寫實體類和轉換方法。而Linq to SharePoint則可以自動將列表映射為實體類,並且可以使用Linq語句來進行查詢,看上去很美!

那么Linq to SharePoint能不能幫我徹底擺脫CAML呢,趁着重構代碼的機會研究了一下,在這里簡單總結一下。

前面說過Linq to SharePoint可以自動生成列表的實體類,這是通過一個叫做SPMetal的工具來實現的,具體的用法請查閱這里

SPMetal會根據實際的列名來生成實體類中的屬性名,所以如果你的列名是中文的話(譬如你安裝了中文版SharePoint),你會得到一份非常詭異且冗長的代碼文件。

當然,如果你能接受中英文混排的代碼的話,這倒也不是什么問題。

SPMetal生成的屬性大多是下面這個樣子的:

[ColumnAttribute(Name = "Body", Storage = "_body", FieldType = "Note")]
public string Body
{
  get
  {
    return this._body;
  }
  set
  {
    if ((value != this._body))
    {
       this.OnPropertyChanging("Body", this._body);
       this._body = value;
       this.OnPropertyChanged("Body");
    }
  }
}
protected string _body;

Body屬性被附加了一個ColumnAttribute,它的作用是將屬性和SharePoint中的某一列關聯起來。在它的命名參數中,Name表示的就是SharePoint中的列名,FieldType指列的類型,Storage表示的是實體類中用來存放列值的變量,可以看到這里為它指定的是一個變量,而不是Body屬性,也就是說,在初始化這一實體的時候,該列的值會直接賦給_body變量,而不經過Body屬性。那么Body屬性的set訪問器又是用來干什么的呢?實際上它的作用只是為了提供一種更改列值的機制,這一點從它復雜的內部流程也能看出端倪。

如果你只是為了查詢方便,並不需要修改和提交數據的話,完全可以使用下面的只讀版本:

[Column(Name = "Body", Storage = "_body", FieldType = "Note")]
public string Body
{
  get
  {
    return this._body;
  }
}
protected string _body;

此外,如果列表的包含一些設置為可空值的列的話,它們會被映射成一個Nullable<T>類型,如下所示:

[ColumnAttribute(Name="RatingCount", Storage="_ratingCount", FieldType="Number")]
public System.Nullable<double> RatingCount{
  get {
    return this._ratingCount;
  }
  set {
    if ((value != this._ratingCount)) {
      this.OnPropertyChanging("RatingCount", this._ratingCount);
      this._ratingCount= value;
      this.OnPropertyChanged("RatingCount");
    }
  }
}
private System.Nullable<double> _ratingCount;

雖然可以理解這么做的原因,但是卻很難接受這種代碼。尤其是在HTML中做綁定時,你不得不針對Nullable屬性額外寫一些代碼來處理它的非空情況。

好在我們可以將屬性本身改成非空的類型,然后在get訪問器里根據情況返回真實的值或者默認值:

[Column(Name="RatingCount", Storage="_ratingCount", FieldType="Counter")]
public double RatingCount{
  get {
    return this._ratingCount ?? 0;
  }
}
private double? _ratingCount;

但要注意Storage指向的變量還得是Nullable類型,以保存列的真實的值(包括空值);屬性的類型雖然可以改為非空類型,但要注意類型一定要和對應的變量相同,因為ColumnAttribute會在初始化時檢查屬性的實際類型。我曾嘗試寫過下面這樣的屬性,結果只收獲了一個異常:

[Column(Name="RatingCount", Storage="_ratingCount", FieldType="Counter")]
public int RatingCount{
  get {
    return this._id ? (int)this._id.Value : 0;
  }
}
private double? _ratingCount;

為什么想要這么做呢?因為我實在想不出投票總數為什么會是一個小數?

如果你剛巧需要使用Linq語句查詢列表,而且查詢條件剛巧也是一個包含可空值的列的話,就不能用上面提到的方法來修改屬性的類型了,否則Linq to SharePoint將無法生成CAML,結果也是以異常告終。

如果想讓可空值列的映射屬性既能在Linq語句里作為條件,又能讓調用者方便使用的話,只能像下面這樣定義它們,對,是它們:

[ColumnAttribute(Name = "RatingCount", Storage = "_ratingCount", FieldType = "Number")]
public double? RatingCountField
{
  get { return _ratingCount; }
}
protected double? _ratingCount;
public int RatingCount { get { return this._ratingCount.HasValue ? (int)this._ratingCount.Value : 0; } }

平常使用int類型的RatingCount,在Linq語句里查詢時使用double?類型的RatingCountField。

坦白說,我很討厭這樣的代碼,兩個含義相同的屬性必然會讓其他閱讀者感到困惑。

此外,如果列是一個查閱項(譬如Author列),我們可以做到映射這個查閱項的完整字符串(譬如“12;#windstyle\chai”),或者查閱項的ID(譬如“12”),或者查閱項的值(譬如“windstyle\chai”),所做的僅僅是在ColumnAttribute里指定IsLookupId或IsLookupValue(如果要拿到完整字符串,則什么都別指定):

[Column(Name = "Author", Storage = "_authorId", FieldType = "Text", IsLookupId = true)]
public int AuthorId
{
  get { return _authorId; }
}
protected int _authorId;

而且如果指定了IsLookupId,就可以在Linq語句中使用這一屬性來做查詢了。

以上提到的都是關於列與屬性的映射,然而有一些列很難通過簡單的映射變成屬性,那就需要另外一種機制:自定義映射。

自定義映射需要實體類實現ICustomMapping接口,並實現它的三個成員方法MapFrom、MapTo和Resolve,我們這里只討論只讀實體類的情況,只需實現MapFrom即可:

[CustomMapping(Columns = new string[] { Attachments" })]
public override void MapFrom(object listItem)
{
  var item = listItem as Microsoft.SharePoint.SPListItem;
  this.IsRootPost = item["IsRootPost"].ToString();
  if (this.IsRootPost == "1")
    this.Url = item.Web.Url + "/" + item.Folder.Url;
  else
    this.Url = new Uri(new Uri(item.Web.Url), item.ParentList.DefaultDisplayFormUrl + "?id=" + item.ID).ToString();
}

MapFrom方法包含一個listItem參數,可以通過它來拿到SPListItem的列值,具體能拿到哪些列,需要在修飾MapFrom的CustomMappingAttribute中指定。

在使用Lambda表達式進行查詢時,CustomMappingAttribute中指定的列名以及之前屬性映射時指定的列名都會成為ViewFields的一員。

但需要注意的是,如果你在Linq語句中使用了通過MapFrom映射而來的屬性,那么它將不會出現在CAML的Query語句中,Linq to SharePoint采取的方法是把所有SPlistItem都獲取並轉換成實體類,然后通過Linq to Objects來進行第二次查詢(而普通的映射屬性則不存在這個問題)。

這當然是極大的性能隱患,然而在Linq to SharePoint中,類似的性能隱患還不止這一處,而且稍不注意就會中招。

譬如根據ID來查找某一Item,我們通常會寫出這樣的代碼:

var item = list.First(i => i.Id == root.ID);

或者

var item = list.Where(i => i.Id == root.ID).First();

或者

var item = list.Where(i => i.Id == root.ID).Single();

這三行代碼看起來沒有任何問題,而且最終也會被翻譯成一模一樣的CAML(我省去了ViewFields):

<View>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name="ID" />
        <Value Type="Counter">16</Value>
      </Eq>
    </Where>
  </Query>
  <RowLimit Paged="TRUE">2147483647</RowLimit>
</View>

注意RowLimit,它的值居然是2147483647,這表示查詢會返回列表中的所有條目,並將它們都轉換成實體類,然后再使用Linq to Objects來進行查詢。

MSDN的這篇文檔中的“Additional Performance Considerations”一節雖然明確了哪些方法會導致這種行為,但First和Single這兩個方法居然都被標記為Efficient。

那么正確的獲取單個條目的查詢表達式該怎么寫呢?使用Take方法,只有Take方法才會正確的翻譯成RowLimit:

this.Root = list.Where(i => i.Id == root.ID).Take(1).Single();

它會被翻譯成(同樣省去了ViewFields):

<View>
  <Query>
    <Where>
      <Eq>
        <FieldRef Name="ID" />
        <Value Type="Counter">16</Value>
      </Eq>
    </Where>
  </Query>
  <RowLimit Paged="TRUE">1</RowLimit>
</View>

同樣的試驗還可以參考這里

此外,我們知道Linq可以使用Take和Skip方法來進行數據分頁,但在Linq to SharePoint中,Skip方法並不會翻譯成CAML的分頁語句,它還是會拿到所有條目。

寫到這里,基本上已經把我所遇到的所有難以接受的部分都介紹完了,其實前面幾條,若不是有嚴重的代碼潔癖的話,也可以不必在意。

最后提到的性能問題才是關鍵所在,而且你很難通過閱讀代碼來發現問題所在,微軟的官方文檔對性能問題的解釋也模棱兩可,一會兒說性能很棒,一會兒又說可能會導致極差的性能。所以最好還是檢查一下每一條查詢語句生成的CAML是否有問題(DataContext的Log屬性會輸出所有翻譯后的CAML)。

現在你應該明白標題的含義了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM