LINQ to Entities 是 LINQ 中最吸引人的部分。它讓你可以使用標准的 C# 對象與數據庫的結構和數據打交道。使用 LINQ to Entities 時,LINQ 查詢在后台轉換為 SQL 查詢並在需要數據的時候執行,即開始枚舉結果的時候執行。LINQ to Entities 還為你獲取的所有數據提供變化追蹤,也就是說,可以修改查詢獲得的對象,然后整批同時把更新提交到數據庫。
LINQ to Entities 是 Entity Framework 的一部分並且取代 LINQ to SQL 作為在數據庫上使用 LINQ 的標准機制。Entity Framework 是行業領先的對象-關系映射(ORM)系統。可以和多種數據庫一起使用,並支持各種靈活、復雜的數據模型。
注:
微軟把開發的重點從 LINQ to SQL 轉移到了 LINQ to Entities,並且宣布 LINQ to SQL 不再提供更新,LINQ to SQL 現在仍被支持單不推薦。
LINQ to Entities 是一項令人印象深刻的技術,但對大多數開發人員而言只是一個小的進步。和 DataSet 一樣,ASP.NET 開發人員使用 LINQ 的查詢特新遠多於它的批量更新特性。這是因為通常 Web 應用程序的更新是單次的而不是批量的。他們更願意在頁面回發時立刻執行更新,同時可以獲得原始值和新(更新)值,這使得通過 ADO.NET 命令提交更新更加方便。
簡而言之,LINQ to Entities 沒有提供任何不能用 ADO.NET代碼、自定義對象、LINQ to Objects 實現的特性,但是有時出於某些原因而需要考慮使用 LINQ to Entities:
- 更少的代碼。不必編寫查詢數據庫的 ADO.NET 代碼,可以通過一個工具生成需要的數據類。
- 靈活的查詢能力。不必拼湊 SQL 語句,而是使用 LINQ 查詢模型。一致的查詢模型可訪問眾多不同的數據源(從數據庫到 XML)。
- 變更追蹤以及批量更新。可以對查詢的數據進行多項修改並提交批量更新,這不需要編寫任何 ADO.NET 代碼。
生成數據模型
Entity Framework 依賴於一個數據模型來使用 LINQ to Entities 進行查詢。表中的行被轉換為 C# 對象的實例,表的列是這些對象的屬性。數據庫架構和數據模型對象的映射是 Entity Framework 的核心.
為了生成模型,右擊 App_Code 目錄,單擊“添加新項”,“ADO.NET 實體數據模型”,設置創建的文件名稱后(這里是 NorthwindModel.edmx),單擊“確定”。
從一個已經存在的數據庫生成模型,即微軟的 Northwind 示例數據庫。配置數據庫連接,並可以選擇表、視圖、和存儲過程。還可以選擇使用復數還是單數形式的對象名(例如,Products 表的行被命名為 Product )、是否包含外鍵關系等。這里選擇全部表並選中“所生成對象的單復數形式”。
Visual Studio 會為你選擇的數據庫元素創建模型圖,它顯示了已經創建的映射對象、對象擁有的字段以及對象之間的關系。
項目中新增了下面這兩個文件:
- NorthwindModel.edmx:這個XML文件定義數據庫模型的架構。
- NorthwindModel.Designer.cs:這個C#代碼文件包含數據模型的映射對象。
數據模型類
我們將把大部分時間花在 NorthwindModel.Designer.cs 這個文件上。因為它包含了我們要用於 LINQ to Entities 查詢的數據類型。(這個文件會被數據模型重新生成,因此不應該也不必要手工去修改這個文件,你的修改會丟失。)
打開該文件,可以看到有兩段代碼區域:Contexts 和 Entities 。
1. 派生的對象上下文
NorthwindModel.Designer.cs 文件中定義的第一個類從 ObjectContext 派生,其名稱是 NorthwindEntities 。這個類的構造函數連接到所生成模型的數據庫,或者你也可以指定連接字符串連接到其他數據庫(必須具有相同的架構,否則模型無法工作)。
下面是一個簡單的示例:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
GridView1.DataSource = db.Products;
GridView1.DataBind();
}
2. 實體類
實體類用於把數據庫表的記錄映射到C#對象。如果選中了“確定所生成對象的單復數形式”選項,那么像 Products 這樣的表創建的實體對象名稱是 Product。
每個實體對象包含如下的內容:
- 工廠方法:可以通過默認的構造函數或工廠方法創建實體對象新實例。工廠方法的參數是需要的字段,它是試圖保存數據元素時防止架構錯誤的好辦法。
- 字段屬性:實體對象為它們派生的數據庫表的每個列包含一個字段屬性。
- 導航屬性:如果數據模型中包含了外鍵關系,實體對象就會包含幫助訪問關聯數據的導航屬性。
提示:
實體類被聲明為分部類,因此可以創建擴展功能,在重新生成數據模型時它不會被覆蓋。
示例:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
var result = from p in db.Products
where p.Discontinued == false
select new
{
ID = p.ProductID,
Name = p.ProductName
};
GridView1.DataSource = result;
GridView1.DataBind();
}
實體關系
實體類包含導航屬性。通過導航屬性可以在數據模型間移動而不需要考慮外鍵關系。看下面的示例:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
var result = from c in db.Customers
let o = from q in c.Orders
where q.Employee.LastName != "King"
select q
where c.City == "London" && o.Count() > 5
select new
{
Name = c.CompanyName,
Contact = c.ContactName,
OrderCount = o.Count()
};
GridView1.DataSource = result;
GridView1.DataBind();
}
這個查詢使用 Orders 導航屬性查詢每個與 Customer 關聯的所有 Orders 。我們使用 Order 實體類型的 Employee 導航屬性檢查下了訂單的員工的姓並過濾掉了姓等於“King”的數據。
使用導航屬性,不必為每個實體類創建單獨的查詢就可以在數據模型間導航。
1. 一對多關系
一對多關系的導航屬性通過強類型的 EntityCollection 來處理。針對某個關系選擇合適記錄的問題你不需要關心,它已經由外鍵關系處理了。因此選擇某個用戶的訂單時,僅僅得到了那些 CustomerID 值和 Customer的CustomerID 屬性值相同的 Order 實例。
使用 SelectMany 擴展方法進行 LINQ to Entities 查詢,可以直接把 EntityCollection 類作為查詢結果,它會在結果集合里包含所有匹配的結果。
示例如下:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
IEnumerable<Order> orders = db.Customers
// 定位 LAZYK 客戶
.Where(c => c.CustomerID == "LAZYK")
// 以上一個客戶結果集為准,導航客戶的 Orders 屬性獲得所有 Order
.SelectMany(c => c.Orders);
GridView1.DataSource = orders;
GridView1.DataBind();
}
也可以改寫成隱式的 LINQ 表達式得到相同的結果:
var result = from o in db.Orders
where o.CustomerID == "LAZYK"
select o;
2. 一對一關系
對於一對一關系,有兩個導航屬性。
- TReference:它返回的結果是 EntityReference<T> ,其中,T 是關聯關系引用的實體類型。例如,Order 實體類型有一個名為 EmployeeReference 的導航屬性,它返回 EntityReference<Employee> 。
- T:這個屬性更有用。T 是它引用的實體類型。例如,Order 實體類型有一個名為 Employee 的方便的導航屬性。
查詢存儲過程
在解決方案資源管理器雙擊 NorthwindModel.edmx 文件,打開數據模型圖,按右鍵選擇“從數據庫更新”可導入存儲過程。打開“實體數據模型瀏覽器”(在“視圖”->“其他”菜單里可以找到),展開 NorthwindModel.Store 節點,打開存儲過程目錄,就會看到導入模型里的存儲過程列表。
選中存儲過程,右鍵添加函數導入,導入界面還可以選擇“創建新的復雜類型”,然后可以如下使用它:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
IEnumerable<ESBC_Result> results
= from c in db.ESBC(DateTime.Now.AddYears(-20), DateTime.Now)
select c;
GridView1.DataSource = results;
GridView1.DataBind();
}
此時返回的類型的名稱就是先前選擇自定義復雜類型的名稱。
LINQ to Entities 查詢揭秘
先前演示的 LINQ to Entities 的用法幾乎和 LINQ to Objects 的用法完全一樣。確實是這樣(至少表面上是這樣)。LINQ 最棒的一件事就是它對各種數據源保持高度的一致性。如果你知道如何使用基本的 LINQ 查詢,就可以用它來查詢對象、數據庫、XML 等。
缺點是這種相似性來自對很多復雜性的隱藏。如果不小心,就會給數據庫產生很大的負載。你應該花時間好好檢查為了服務你的 LINQ to Entities 查詢,究竟生成了什么樣的 SQL 查詢。通過 Entity Framework 查看 SQL 查詢並不容易,需要把 LINQ to Entities 查詢的結果轉換為 System.Data.Objects.ObjectQuery 的實例並調用 ToTraceString()方法才行。
示例:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
var result = from c in db.Customers
let o = from q in c.Orders
where q.Employee.LastName != "King"
select q
where c.City == "London" && o.Count() > 5
select new
{
Name = c.CompanyName,
Contact = c.ContactName,
OrderCount = o.Count()
};
Label1.Text = (result as System.Data.Objects.ObjectQuery).ToTraceString();
}
很多時候像這樣打印 SQL 查詢並不現實。如果使用的是非 Express 版本的 SQL Server,可以使用 SQL Server Profile 工具。如果是 Express 版本的,那我們推薦使用 Anjlab 開發的開源免費的 SQL Profile,它很不錯。
1. 遲到的過濾
一個導致不必要的數據庫查詢的常見原因是過濾查詢中的數據太晚了,這是一個查詢示例:
NorthwindEntities db = new NorthwindEntities();
IEnumerable<NorthwindModel.Customer> custs
= from c in db.Customers
where c.Country == "UK"
select c;
IEnumerable<NorthwindModel.Customer> results
= from c in custs
where c.City == "London"
select c;
GridView1.DataSource = results;
GridView1.DataBind();
這里的問題是,第一個查詢從數據庫檢索 Country=UK 的所有記錄。第二個查詢應用於第一個查詢的結果,但是它使用的是 LINQ to Objects,也就是說我們丟棄了從數據庫請求的絕大部分數據(第一個查詢就顯得非常的浪費資源了)。
這個示例只會產生一條 SQL 查詢,類似於於下面:
SELECT * FROM Customers WHERE Country='UK'
解決方案是把過濾器融合到同一個查詢里。
2. 使用延遲和貪婪數據加載
為了讓導航屬性無縫地工作,LINQ to Entities 使用了一項稱作延遲加載的技術,只在需要的時候才從數據庫加載數據。通過導航屬性從某個實體類型轉移到另一個實體類型時,第二個實體類型的實例僅在需要的時候才加載。
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
IEnumerable<NorthwindModel.Customer> custs
= from c in db.Customers
where c.Country == "UK" && c.City == "London"
select c;
List<string> names = new List<string>();
foreach (NorthwindModel.Customer c in custs)
{
if (c.Orders.Count > 2)
{
names.Add(c.CompanyName);
}
}
GridView1.DataSource = names;
GridView1.DataBind();
}
這個查詢中,我們過濾出一組 Customers,然后對其結果進行迭代,並導航到相關的 Order 實例,最終,我們得到了位於英國倫敦且訂單多余兩筆的公司名稱。
由於延遲加載,Orders 表的數據只在需要時加載,也就是說為了在循環中得到每個客戶關聯的訂單,我們都生成了一條 SQL 查詢。這產生了太多的查詢。對於這個簡單的示例,我們可以把所有這一切整合到一個 LINQ 查詢里。
但其實我們要演示的是貪婪加載功能。它可以在查詢中加載其他表的關聯數據。示例如下:
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
IEnumerable<NorthwindModel.Customer> custs
= from c in db.Customers.Include("Orders")
where c.Country == "UK" && c.City == "London"
select c;
List<string> names = new List<string>();
foreach (NorthwindModel.Customer c in custs)
{
if (c.Orders.Count > 2)
{
names.Add(c.CompanyName);
}
}
GridView1.DataSource = names;
GridView1.DataBind();
}
使用 Include()擴展方法包含關聯的數據,它告訴 LINQ to Entities 引擎關聯到我們查詢的 Customer 的 Order 實例應該被加載,即便這個查詢並沒有直接關聯到 Orders 表。最終,Entity Framework 捕獲了結果,也就是說當我們迭代 Customer 實例並檢查關聯的 Orders 時,它們都已經被加載了,不需要再生成額外的數據庫查詢。
3. 使用顯式加載
如果要完全控制加載的數據,可以使用顯式加載。可以使用派生 ObejectContext 類禁用延遲加載,然后使用 EntityCollection.Load()方法按需加載數據,可以通過 IsLoaded 方法檢查所需的數據是否已經加載。
protected void Page_Load(object sender, EventArgs e)
{
NorthwindEntities db = new NorthwindEntities();
db.ContextOptions.LazyLoadingEnabled = false;
IEnumerable<NorthwindModel.Customer> custs
= from c in db.Customers
where c.Country == "UK"
select c;
foreach (NorthwindModel.Customer c in custs)
{
if (c.City == "London")
{
c.Orders.Load();
}
}
List<Order> orders = new List<Order>();
foreach (NorthwindModel.Customer c in custs)
{
if (c.Orders.IsLoaded)
{
orders.Add(c.Orders.First());
}
}
GridView1.DataSource = orders;
GridView1.DataBind();
}
- 禁用了延遲加載,即導航屬性要引用的數據不會被自動加載。
- 第一次迭代使用 Load()顯式加載那些符合條件的 Orders 數據,此時數據會從數據庫加載到 Entity Framework 緩存里。
- 第二次迭代檢查所有的 Customers 對象,用 IsLoaded 屬性判斷哪些 Customers 加載了 Orders 數據
- First()方法可以把第一條 Order 添加到集合中
這個示例並不怎么自然,但它足以讓你看出項目中實際使用顯式加載所需的知識。
4. 編譯查詢
LINQ to Entities 另一個隱藏的功能是能夠創建已編譯的查詢。已編譯的查詢是一個強類型的 Func 委托,它有一個用於查詢的參數。編譯查詢時,執行翻譯到 SQL 語句的動作,以后每次調用已編譯的查詢時都會對其重用。
這並不像使用存儲過程那樣高效,因為數據庫還是要創建查詢計划來執行 SQL ,但它確實避免了 LINQ to Entities 重復解析 LINQ 查詢。
示例:
using System.Data.Objects;
public partial class Chapter13_DerivedObjectContext : System.Web.UI.Page
{
// 封裝一個具有兩個參數並返回 TResult 參數指定的類型值的方法。
Func<NorthwindEntities, string, IQueryable<NorthwindModel.Customer>> MyCompiledQuery;
NorthwindEntities db;
protected void Page_Load(object sender, EventArgs e)
{
// CompiledQuery 表示一個緩存的 LINQ to Entities 查詢
// Compile() 創建一個表示已編譯的 LINQ to Entities 查詢的新委托
MyCompiledQuery = CompiledQuery.Compile<NorthwindEntities, string,
IQueryable<NorthwindModel.Customer>>((context, city) =>
from c in context.Customers
where c.City == city
select c);
db = new NorthwindEntities();
GridView1.DataSource = MyCompiledQuery(db, "London");
GridView1.DataBind();
}
}





