.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170518303
出刊日期: 2017/5/31
Entity Framework提供了LINQ to Entities,以讓程式設計師利用LINQ語法查詢資料庫的內容。LINQ to Entities提供了相當多的語法來載入資料,這些語法略有差異,了解這些不同語法的差異有助於撰寫效能更佳的應用程式。這篇文章將介紹一些常用的查詢語法,並了解它們的運用。
本文延續使用《Change Tracking API - 1》一文建立的ADO.NET實體資料模型來說明Entity Framework提供的查詢語法。
列舉DbSet物件查詢資料
每當你列舉DbSet物件中的内容,Entity Framework就會送出一個查詢到資料庫,載入資料庫資料表最新的資料。參考以下範例程式碼,查詢Pubs資料庫stores資料表資料,程式碼使用到兩個foreach方法,印出DbSet中store物件的屬性,這會促使Entity Framework下兩次資料庫查詢。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
foreach (var store in context.stores)
{
Console.WriteLine(store.stor_name);
}
foreach (var store in context.stores)
{
Console.WriteLine(store.stor_id);
}
}
}
}
}
你可以使用Visual Studio Diagnostic Tools來觀察程式的執行,從「Debug」->「Windows」->「Show Diagnostic Tools」,然後再按F5執行程式,執行到foreach方法,Entity Framework就送出以下查詢到資料庫
USE [pubs];
GO
SELECT
[Extent1].[stor_id] AS [stor_id],
[Extent1].[stor_name] AS [stor_name],
[Extent1].[stor_address] AS [stor_address],
[Extent1].[city] AS [city],
[Extent1].[state] AS [state],
[Extent1].[zip] AS [zip]
FROM [dbo].[stores] AS [Extent1]
參考下圖,每一次執行到foreach列舉DbSet物件的內容時,就會攔截到一個送到資料庫的查詢事件:
圖 1:使用Diagnostic Tools監看程式執行。
若要考慮到執行效能,避免重複執行資料庫實體查詢的動作,可以善用LINQ提供的To開頭的方法,將查詢結果複製到記憶體。參考以下範例程式碼,叫用ToList()方法將資料複製到List<store>物件,後續便可以從此集合中找尋資料:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var storeList = context.stores.ToList();
foreach (var store in storeList)
{
Console.WriteLine(store.stor_name);
}
foreach (var store in storeList)
{
Console.WriteLine(store.stor_id);
}
}
}
}
}
參考以下範例程式碼,則是使用ToArray()方法,將資料複製到store[]陣列之中,後續便可以從此陣列找尋資料:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var storeList = context.stores.ToArray();
foreach (var store in storeList)
{
Console.WriteLine(store.stor_name);
}
foreach (var store in storeList)
{
Console.WriteLine(store.stor_id);
}
}
}
}
}
使用DbSet物件ToLookup()方法
因為Entity Framework GroupBy()方法回傳的是IQueryable<IGrouping<TKey, TSource>>泛型介面,IQueryable有延遲查詢(deferred execution)的特性,只要列舉此介面,就會建立並執行一個資料庫的實體查詢。以下這段程式碼,每次執行到foreach都會建立資料庫實際連線(以本例來說執行兩次),查詢資料庫資料。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var storeGroup = context.stores.GroupBy(s => s.state);
foreach (var group in storeGroup)
{
Console.WriteLine($"Group : {group.Key}");
foreach (store item in group)
{
Console.WriteLine($"\t{item.stor_name}");
}
}
foreach (var group in storeGroup)
{
Console.WriteLine($"Group : {group.Key}");
foreach (store item in group)
{
Console.WriteLine($"\t{item.stor_id}");
}
}
}
}
}
}
每次列舉IQueryable會讓Entity Framework產生以下查詢讀取資料:
USE [pubs];
GO
SELECT
[Project2].[C1] AS [C1],
[Project2].[state] AS [state],
[Project2].[C2] AS [C2],
[Project2].[stor_id] AS [stor_id],
[Project2].[stor_name] AS [stor_name],
[Project2].[stor_address] AS [stor_address],
[Project2].[city] AS [city],
[Project2].[state1] AS [state1],
[Project2].[zip] AS [zip]
FROM ( SELECT
[Distinct1].[state] AS [state],
1 AS [C1],
[Extent2].[stor_id] AS [stor_id],
[Extent2].[stor_name] AS [stor_name],
[Extent2].[stor_address] AS [stor_address],
[Extent2].[city] AS [city],
[Extent2].[state] AS [state1],
[Extent2].[zip] AS [zip],
CASE WHEN ([Extent2].[stor_id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
FROM (SELECT DISTINCT
[Extent1].[state] AS [state]
FROM [dbo].[stores] AS [Extent1] ) AS [Distinct1]
LEFT OUTER JOIN [dbo].[stores] AS [Extent2] ON ([Distinct1].[state] = [Extent2].[state]) OR (([Distinct1].[state] IS NULL) AND ([Extent2].[state] IS NULL))
) AS [Project2]
ORDER BY [Project2].[state] ASC, [Project2].[C2] ASC
為了效能,你可以改用DbSet物件ToLookup()方法,參考以下範例程式碼,只在ToLookup()這行程式執行一次實體資料庫查詢,將查詢結果放到記憶體,後續foreach語法的程式碼便可由記憶體中取得資料。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var storeGroup = context.stores.ToLookup(s => s.state);
foreach (var group in storeGroup)
{
Console.WriteLine($"Group : {group.Key}");
foreach (store item in group)
{
Console.WriteLine($"\t{item.stor_name}");
}
}
foreach (var group in storeGroup)
{
Console.WriteLine($"Group : {group.Key}");
foreach (store item in group)
{
Console.WriteLine($"\t{item.stor_id}");
}
}
}
}
}
}
使用DbSet物件Find()方法
若想要找尋一個Entity Framework 放在記憶體中的物件,可以利用DbSet物件提供的Find()方法。Find()方法可以傳入key值當做搜尋的參數,找尋並回傳相符的物件,若找不到key相符的物件,Find()方法就回傳「null」。key值對應到主鍵(Primary)欄位。若key值是一個組合鍵,Find()方法可以按「,」符號區隔,填入組成key值的屬性名稱。
不是每一次叫用Find()方法時,都會從資料庫載入資料,而是按照以下優先順序來搜尋物件:
- · 從記憶體找,搜尋DbSet物件中是否有包含key相符的Entity物件,並回傳此Entity物件。此Entity物件的資料可以從資料庫載入,或是一個新建立、附加到DbSet屬性,但尚未儲存到資料庫的Entity物件。
- · 從資料庫載入Entity物件,並回傳此物件。
參考以下範例程式碼,找尋是否有key值為「6380」store資料,範例中建立DbContext物件,然後叫用DbSet的Find()方法找Entity。若使用「Visual Studio Diagnostic Tools」來觀察程式的執行,第一次執行Find()方法時,會建立實體資料庫連線,從資料庫載入資料。但第二次執行Find()方法時,就不會執行資料庫查詢,而是從記憶體將DbSet中key相符的物件回傳:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
store s1 = context.stores.Find("6380"); // DB Query
Console.WriteLine(s1.stor_name);
store s2 = context.stores.Find("6380"); // From memory
Console.WriteLine(s1.stor_name);
}
}
}
}
Find()方法會讓Entity Framework產生以下查詢來取回資料,使用了「Select Top (2)」語法,因為Find()方法的參數對應到資料表主鍵欄位,不應該找回重複的兩筆資料,藉由「Select Top (2)」語法可以進行資料驗證的動作:
USE [pubs];
GO
--Type and value data was not available for the following variables. Their values have been set to defaults.
DECLARE @p0 AS SQL_VARIANT;
SET @p0 = NULL;
SELECT TOP (2)
[Extent1].[stor_id] AS [stor_id],
[Extent1].[stor_name] AS [stor_name],
[Extent1].[stor_address] AS [stor_address],
[Extent1].[city] AS [city],
[Extent1].[state] AS [state],
[Extent1].[zip] AS [zip]
FROM [dbo].[stores] AS [Extent1]
WHERE [Extent1].[stor_id] = @p0
我們看另一個例子,參考以下範例程式碼, 建立一個store物件,並將它加到context.stores之中,資料庫目前並沒有key值為「9999」的資料,因此兩個Find()方法實際上是直接搜尋記憶體,將新建立的store物件回傳,並沒有執行資料庫查詢。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
store s = new store() { stor_id = "9999", stor_name = "9999 store" };
context.stores.Add(s);
store s1 = context.stores.Find("9999"); // From memory
Console.WriteLine(s1.stor_name);
store s2 = context.stores.Find("9999"); // From memory
Console.WriteLine(s1.stor_name);
}
}
}
}
使用DbSet物件SingleOrDefault()方法
假設每此找尋單一物件時,都要查詢資料庫的資料,則可以改用DbSet物件的Single()或SingleOrDefault()方法。Single()或SingleOrDefault()方法的差異是:Single()方法找不到條件相符的資料會產生例外錯誤;而SingleOrDefault()方法找不到條件相符的資料時不會產生例外錯誤,而是回傳「null」。
參考以下範例程式碼,叫用SingleOrDefault()方法找尋city等於「Seattle」的商店資訊,範例中叫用DbSet物件SingleOrDefault()方法兩次,使用Visual Studio Diagnostic Tools來觀察程式的執行,你將發現每次叫用時,都會執行資料庫查詢:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var q = from s in context.stores
where s.city == "Seattle"
select s;
var r = q.SingleOrDefault(); //DB Query
r = q.SingleOrDefault(); //DB Query
Console.WriteLine(r.stor_name);
}
}
}
}
SingleOrDefault()方法會讓Entity Framework產生以下SQL語法來查詢資料庫資料:
--The data may be truncated and may not represent the query that was run on the server
USE [pubs];
GO
SELECT TOP (2)
[Extent1].[stor_id] AS [stor_id],
[Extent1].[stor_name] AS [stor_name],
[Extent1].[stor_address] AS [stor_address],
[Extent1].[city] AS [city],
[Extent1].[state] AS [state],
[Extent1].[zip] AS [zip]
FROM [dbo].[stores] AS [Extent1]
WHERE 'Tapipei' = [Extent1].[city]
特別注意,Entity Framework使用了「Select Top (2)」語法來查詢資料,若查詢回傳兩筆紀錄,則SingleOrDefault()方法便會觸發System.InvalidOperationException例外錯誤,錯誤訊息為「Sequence contains more than one element」,請參考下圖所示:
圖 2:觸發System.InvalidOperationException例外錯誤。
只要叫用SingleOrDefault()方法,就會執行資料庫查詢,而不找尋新增到DbSet物件但尚未寫入資料庫的資料。參考以下範例程式碼,新建立一個Store物件,並將之加入DbSet物件中,但程式執行到SingleOrDefault()方法會回傳「null」,因此最後一行程式將會觸發NullReferenceException例外錯誤。
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
store s = new store() { stor_id = "9999", stor_name = "9999 store" };
context.stores.Add(s);
var r = context.stores.SingleOrDefault(o => o.stor_name == "9999 store"); //DB Query
Console.WriteLine(r.stor_name); // System.NullReferenceException'
}
}
}
}
使用DbSet物件Single()方法
DbSet物件Single()方法和SingleOrDefault()方法運作方式大致相同,不再贅述,參考以下範例程式碼:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var q = from s in context.stores
where s.city == "Seattle"
select s;
var r = q.Single(); //DB Query
r = q.Single(); //DB Query
Console.WriteLine(r.stor_name);
}
}
}
}
使用Single()方法將產生以下查詢:
USE [pubs];
GO
SELECT TOP (2)
[Extent1].[stor_id] AS [stor_id],
[Extent1].[stor_name] AS [stor_name],
[Extent1].[stor_address] AS [stor_address],
[Extent1].[city] AS [city],
[Extent1].[state] AS [state],
[Extent1].[zip] AS [zip]
FROM [dbo].[stores] AS [Extent1]
WHERE 'Seattle' = [Extent1].[city]
使用DbSet物件First()與FirstOrDefault ()方法
使用DbSet物件的Single()或SingleOrDefault()方法找詢資料時,若滿足篩選條件的資料有兩筆以上會觸發System.InvalidOperationException例外錯誤,若不在乎回傳資料的筆數,而想取得回傳資料的第一筆,可以改用DbSet物件的First()與FirstOrDefault ()方法。First ()與FirstOrDefault()方法的差異是:First ()方法找不到條件相符的資料會產生例外錯誤;而FirstOrDefault()方法找不到條件相符的資料時不會產生例外錯誤,而是回傳「null」。
只要叫用FirstOrDefault()方法,就會執行資料庫查詢,而不找尋新增到DbSet但尚未寫入資料庫的資料。參考以下範例程式碼:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
var q = from s in context.stores
where s.city == "Seattle"
select s;
var r = q.FirstOrDefault(); //DB Query
r = q.FirstOrDefault(); //DB Query
Console.WriteLine(r.stor_name);
}
}
}
}
使用FirstOrDefault()方法將產生以下查詢:
USE [pubs];
GO
SELECT TOP (1)
[Extent1].[stor_id] AS [stor_id],
[Extent1].[stor_name] AS [stor_name],
[Extent1].[stor_address] AS [stor_address],
[Extent1].[city] AS [city],
[Extent1].[state] AS [state],
[Extent1].[zip] AS [zip]
FROM [dbo].[stores] AS [Extent1]
WHERE 'Seattle' = [Extent1].[city]
查詢本機資料
DbSet物件包含一個Local屬性,記錄從資料庫查詢回來的所有資料。此外此屬性也會記錄新增到DbSet物件但尚未寫回資料庫的Entity,不過並不會記錄被標示為刪除而實際上還存在於資料庫的Entity物件。
參考以下範例程式碼,一開始context.stores.Local.Count屬性的值是「0」,新增一個Store物件到DbSet物件,則Count屬性的值是「1」;使用FirstOrDefault()方法載入一筆資料,則Count屬性的值是「2」,從DbSet物件移除一個物件,則Count屬性的值是「1」。最後利用迴圈印出Local屬性所有的物件資料:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
store s = new store() { stor_id = "9999", stor_name = "9999 store" };
context.stores.Add(s);
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 1
store storeToDelete = context.stores.FirstOrDefault(o => o.stor_id == "6380");
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 2
context.stores.Remove(storeToDelete);
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 1
foreach (var store in context.stores.Local)
{
Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
}
}
}
}
}
執行測試程式碼,此範例執行結果如下所示:
圖 3:查詢本機資料。
載入資料到本機
若想要將資料庫資料表所有資料載入到本機,只要列舉DbSet物件就會將資料庫資料載入記憶體,並且轉換成Entity物件放在Local屬性中。參考以下範例程式碼,一開始context.stores.Local.Count屬性的值是「0」,使用foreach列舉DbSet物件store屬性,context.stores.Local.Count屬性的值便變為「6」;最後一段foreach將Local屬性中的物件資料印出:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
foreach (var store in context.stores)
{
Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
}
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 6
Console.WriteLine("=============");
Console.WriteLine("Local Data :");
foreach (var store in context.stores.Local)
{
Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
}
}
}
}
}
執行測試程式碼,此範例執行結果如下所示:
圖 4:載入資料到本機。
除了使用列舉DataSet這招來載入資料之外,還有一個Load()方法可以使用`,參考以下範例程式碼:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
context.stores.Load();
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 6
Console.WriteLine("=============");
Console.WriteLine("Local Data :");
foreach (var store in context.stores.Local)
{
Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
}
}
}
}
}
執行測試程式碼,此範例執行結果如下所示:
圖 5:使用Load()方法載入資料。
若不想一次載入資料表所有資料,Load()方法可以搭配LINQ查詢,參考以下範例程式碼,載入state為「CA」的資料:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
var q = from s in context.stores
where s.state == "CA"
select s;
q.Load();
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 3
Console.WriteLine("=============");
Console.WriteLine("Local Data :");
foreach (var store in context.stores.Local)
{
Console.WriteLine($" {store.stor_id} - {store.stor_name} - {store.state}");
}
}
}
}
}
執行測試程式碼,此範例執行結果如下所示:
圖 6:使用Load()方法載入部分資料表資料。
我們可以叫用Load()方法多次,分批將資料載入,參考以下範例程式碼,第一次Load()方法載入三筆State為「CA」的資料;第二個Load()方法載入筆State為「WA」的資料,第二次叫用Load()方法載入資料時,不會清空Local屬性,新查詢出來的資料會附加到Local屬性:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PubsDemo
{
class Program
{
static void Main(string[] args)
{
using (var context = new PubsContext())
{
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
var q = from s in context.stores
where s.state == "CA"
select s;
q.Load();
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 3
q = from s in context.stores
where s.state == "WA"
select s;
q.Load();
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 5
Console.WriteLine("=============");
Console.WriteLine("Local Data :");
foreach (var store in context.stores.Local)
{
Console.WriteLine($" {store.stor_id} - {store.stor_name} - {store.state}");
}
}
}
}
}
執行測試程式碼,此範例執行結果如下所示:
圖 7:使用Load()方法批次載入資料。