上個月當我的本地 .NET 用戶組的演示者正在課堂上寫 LINQ 查詢時,我問他 ,“以前沒有 LINQ 的時候,我們是怎麼過的”?他回答說,“真是難以想象” 。
這是真的。自從 2008 年 LINQ 被引入 Visual Studio 後,它對我們在 Microsoft .NET Framework 中的編程方式產生了如此 重大的影響。與 Visual Basic 和 C# 中引入的許多新的語言功能相結合,LINQ 可以前後一致地解決查 詢內存中對象和數據來源的問題。
LINQ 具備將形狀隨機的數據投影到匿名類型的功能,這種功能既有好的一面 ,也有不好的一面。如果您只需要獲取數據的特定視圖,而不必為此一次性的類 型聲明新類,匿名類型是一個不錯的解決方案。LINQ 的投影和匿名類型的確是把 我們寵壞了。那麼,為什麼我說,它們也有不好的一面呢?
如果您曾經將 LINQ 投影用於需要將數據返回另一方法的某種方法,或者更糟 糕,將 LINQ 投影用在 Windows Communication Foundation (WCF) 服務操作中 ,對此您可能會有所了解。
原因即在於匿名類型是一次性類型,它們沒有聲明,只有創建它們的方法可以 理解它們。如果您寫了一個返回一列匿名類型的查詢,因為沒有辦法表達“匿名 類型”,所以沒有辦法定義某個方法參數,說:“我將返回一列...”。
以下是一個采用簡單投影的 LINQ to Entities 查詢:
var custQuery = from c in context.Customers
select new {c.CustomerID, Name=c.LastName.Trim() +
", " + c.FirstName};
在運行時,custQuery 變量將實際成為某個 ObjectQuery<<>f__AnonymousType0<int,string>>。
有了 var(以及 Visual Basic Dim 的替代使用)我們不再需要找到這種非類 型的表達方式。
如果您想從某個方法返回該查詢的結果,唯一合理的解決方案是創建代表要返 回的類型的類。不過,這樣做,提供匿名類型將毫無意義。現在您必須寫更多的 代碼,定義類,還可能需要定義容納新類的新項目,並確保使用這些類的程序集 能訪問到它們等等。
最近,數據服務又給出了一道難題。為了對數據進行投影,您必須在服務中創 建自定義的操作,執行自己的查詢,然後返回某一預先定義的、可以為客戶端理 解的類。
在您處理服務時,很多情況下您都希望在無需通過線路移動大規模類型的情況 下,處理數據的特定視圖。
為了滿足這一臨時性要求,除了在您的域中創建額外的類型之外,您還有更多 的選擇。
WCF 數據服務中的新投影功能
.NET Framework 3.5 SP1 的數據服務更新為 WCF 數據服務引入了少數幾個強 大的功能,這也是 .NET Framework 4 的組成部分。這些功能中就有針對數據服 務在查詢中使用投影的功能。強烈建議您查看 WCF 數據服務團隊的博客帖子 (blogs.msdn.com/astoriateam/archive/2010/01/27/data-services-update- for-net-3-5-sp1-available-for-download.aspx),以了解這次更新的所有新功 能。
數據服務 URI 語法中添加了 $select 運算符。它允許使用屬性甚至是導航屬 性投影。
下面簡單舉例說明了隨 SalesOrderHeaders 導航屬性獲取客戶的幾個標量屬 性的投影:
http://localhost /DataService.svc/Customers(609)
$select=CustomerID,LastName,FirstName,SalesOrderHeaders&$expand=
SalesOrderHeaders
擴展運算符強制結果不僅包含到這些訂單的鏈接,還包含每個訂單的數據。
圖 1 顯示了此查詢的結果。擴展的 SalesOrderHeaders(只包含一個訂單) 以黃色突出顯示,而客戶信息以綠色突出顯示。
圖 1 請求三個客戶屬性和客戶的 SalesOrderHeaders 的數據服務查詢投影的 結果
.NET Framework 中的 LINQ to REST 功能以及 WCF 數據服務的 Silverlight 客戶端 API 已得到了更新,也允許使用投影:
var projectedCust = (from c in context.Customers
where c.CustomerID==609
select new {c.CustomerID, c.LastName})
.FirstOrDefault();
ProjectedCust 現在是一個可供用於客戶端應用程序的匿名類型。
它還可能投影到已知實體類型,而且在某些情況下,DataContext 可以跟蹤由 客戶端所做的更改,這些更改還可以通過服務的 SaveChanges 方法保留下來。請 注意,任何缺少的屬性將被填充其默認值(或填充為空,如果它們可為空值), 並保留到數據庫。
自 EDM 啟用投影強類型
如果您使用實體框架實體數據模型 (EDM),可使用某種方便易用的方法,在您 需要從創建匿名類型的方法中將它們傳遞出時避免被卡住。
EDM 有名為 QueryView 的映射。我過去在提供數據服務投影支持前,曾向很 多客戶指明這一點。它不僅可以為數據服務很好地解決這個問題,還能為自定義 WCF 服務和 RIA 服務解決問題。
什麼是 QueryView?這是實體框架元數據中的一種特殊映射類型。如圖 2 所 示,通常,您會根據元數據的存儲模型存儲架構定義語言 (SSDL) 的說明,將實 體的屬性映射到數據庫表或視圖列。
圖 2 直接將表列映射到實體屬性
與此相反,QueryView 會讓您通過這些 SSDL 表列創建視圖,而不是直接映射 到它們。使用 QueryView 有多個理由。一些例子包括:采用只讀方式公開實體, 以條件映射不允許的方式過濾實體,或提供數據庫中數據表的不同視圖。
針對上述目的的最後一個,我將重點關注您經常會在自己的應用程序中投影的 匿名類型的替代方案。參數選用表即是其中一個例子。為什麼要為只需要 ID 和 客戶姓名的下拉菜單返回全部客戶類型?
生成 QueryView
創建 QueryView 之前,您需要在模型中創建代表您所針對的視圖形狀的實體 ,如 CustomerNameAndID 實體。
但是您不能將這個實體直接映射到 SSDL 中的 Customer 表。將 Customer 實 體和 CustomerNameAndID 實體都映射到表的 CustomerID 列會產生沖突。
正如您可以在數據庫中創建表視圖,您可以改為直接在元數據中創建 SSDL Customer 視圖。QueryView 就是 SSDL 上的 Entity SQL 表達式。它是模型的映 射規范語言 (MSL) 元數據的一部分。創建 QueryView 時無法獲得設計器支持, 所以您需要直接在 XML 中鍵入。
因為您要映射到表的存儲架構,所以最好看看其外觀。圖 3 列出了 Customer 數據庫表的 SSDL 描述,除了使用了提供商數據類型之外,它與概念模型的元數 據的 Customer 實體類似。
圖 3 數據庫 Customer 表的 SSDL 描述
<EntityType Name="Customer">
<Key>
<PropertyRef Name="CustomerID" />
</Key>
<Property Name="CustomerID" Type="int" Nullable="false"
StoreGeneratedPattern="Identity" />
<Property Name="Title" Type="nvarchar" MaxLength="8" />
<Property Name="FirstName" Type="nvarchar" Nullable="false"
MaxLength="50" />
<Property Name="MiddleName" Type="nvarchar" MaxLength="50" />
<Property Name="LastName" Type="nvarchar" Nullable="false"
MaxLength="50" />
<Property Name="Suffix" Type="nvarchar" MaxLength="10" />
<Property Name="CompanyName" Type="nvarchar" MaxLength="128" />
<Property Name="SalesPerson" Type="nvarchar" MaxLength="256" />
<Property Name="EmailAddress" Type="nvarchar" MaxLength="50" />
<Property Name="Phone" Type="nvarchar" MaxLength="25" />
<Property Name="ModifiedDate" Type="datetime" Nullable="false" />
<Property Name="TimeStamp" Type="timestamp" Nullable="false"
StoreGeneratedPattern="Computed" />
</EntityType>
存儲架構的命名空間 ModelStoreContainer 是 QueryView 的另一個重要元素 。現在,您已經擁有了構建 QueryView 表達式的所有必要元件。以下 QueryView 將三個必填字段從 SSDL 投影到我在模型中創建的 CustomerNameAndID 實體:
SELECT VALUE AWModel.CustomerNameAndID(c.CustomerID, c.FirstName,
c.LastName) FROM ModelStoreContainer.Customer as c
我們來描述下該實體 SQL:“查詢存儲架構中的客戶,取出這三列,讓他們作 為 CustomerNameAndID 實體返回給我。”AWModel 是概念模型的實體容器的命名 空間。對於表達式中引用的概念架構定義語言 (CSDL) 和 SSDL 類型,您需要使 用其強類型化名稱。
只要投影的結果(整數、字符串與字符串)與目標實體的架構相匹配,映射將 成功。我試過在投影內使用函數和連接(如 (c.CustomerID, c.FirstName + c.LastName)),但沒有成功,並收到了不允許使用這些函數的錯誤消息。因此, 我被迫使用 FirstName 和 LastName 屬性,而讓客戶端處理連接問題。
將 QueryView 置入元數據
對於進入 EntityContainerMapping 的實體,您必須將 EntitySetMapping 元 素內的 QueryView 表達式置入元數據。圖 4 在我的 EDMX 文件的原始 XML 中顯 示了這一 QueryView(以黃色突出顯示)。
圖 4 映射部分的 QueryView
現在我的 CustomerNameAndID 已成為我的模型的一部分,可提供給任何消費 者。此 QueryView 還有另一優勢。盡管這一 QueryView 的目標是創建只讀引用 列表,您還可以使用 QueryView 更新所映射的實體。上下文將跟蹤 CustomerNameAndID 對象的變化。雖然實體框架不能夠為這一實體自動生成插入 、更新和刪除命令,您可以將存儲過程映射到它。
受惠於 QueryView
既然您已經在模型中加入了 QueryView,您就不再需要依靠投影或匿名類型來 檢索數據的這些視圖。如下所示,在 WCF Data Services 中, CustomerNameAndIDs 將成為可供查詢的有效實體集:
List<CustomerNameAndID> custPickList =
context.CustomerNameAndIDs.ToList();
再沒有雜亂的投影了;更有優勢的一點是,您不需要在應用程序中定義新的類 型,並投影到它們,即可在自定義 WCF 服務中創建現在可以返回此強類型化對象 的服務操作。
public List<CustomerNameAndID> GetCustomerPickList()
{
using (var context = new AWEntities())
{
return context.CustomerNameAndIDs.OrderBy(
c => c.LastName).ToList();
}
}
因受到限制,我們無法連接 QueryView 中的名字和姓氏,要由使用此服務的 開發人員一方實現這個連接。
WCF RIA 服務也可受益於 QueryView。您可能希望公開某種方法,從您的域服 務中檢索某家餐廳的參數選用表。您不必在域服務中創建額外的類來表示這些投 影的屬性,該 RestaurantPickList 實體可獲得模型中的 QueryView 支持,輕松 提供這一數據:
public IQueryable<RestaurantPickList> GetRestaurantPickList()
{
return context.RestaurantPickLists;
}
是選擇 QueryViews 還是投影,我們已經算面面俱到了
有能力通過您的數據類型對視圖進行投影是查詢方面的巨大優勢,這是 WCF 數據服務增加的一項很不錯的功能。即便如此,如果有時不需要進行投影,也不 必擔心結果共享,即可訪問這些視圖,將簡化您的某些編碼任務。
最後一個注意事項:隨著在實體框架的 .NET Framework 4 版本中引入外鍵, 因為您可以返回只讀實體,並輕松使用它們的屬性來更新正在編輯的實體中的外 鍵屬性,QueryView 參數選用表將更有意義。
下載示例代碼:http://code.msdn.microsoft.com/mag201005DataPoints