直接對 DbSet 執行 LINQ 查詢時,查詢一定會傳送到資料庫,但你可以使用 DbSet.Local 屬性存取目前記憶體中的資料。 你也可以使用 DbContext.Entry 和 DbContext.ChangeTracker.Entries 方法,存取 EF 為您的實體所追蹤的額外資訊。 本主題中展示的技術同樣適用於使用 Code First 與 EF Designer 所建立的模型。
使用 Local 來查看本地資料
DbSet 的本地屬性提供對目前被上下文追蹤且未標記為刪除的集合實體的簡單存取。 存取本地屬性時,查詢不會被傳送到資料庫。 這表示它通常在查詢已經完成後才使用。 Load 擴充方法可用來執行查詢,讓上下文追蹤結果。 例如:
using (var context = new BloggingContext())
{
// Load all blogs from the database into the context
context.Blogs.Load();
// Add a new blog to the context
context.Blogs.Add(new Blog { Name = "My New Blog" });
// Mark one of the existing blogs as Deleted
context.Blogs.Remove(context.Blogs.Find(1));
// Loop over the blogs in the context.
Console.WriteLine("In Local: ");
foreach (var blog in context.Blogs.Local)
{
Console.WriteLine(
"Found {0}: {1} with state {2}",
blog.BlogId,
blog.Name,
context.Entry(blog).State);
}
// Perform a query against the database.
Console.WriteLine("\nIn DbSet query: ");
foreach (var blog in context.Blogs)
{
Console.WriteLine(
"Found {0}: {1} with state {2}",
blog.BlogId,
blog.Name,
context.Entry(blog).State);
}
}
如果資料庫中有兩個部落格——「ADO.NET 部落格」BlogID 為 1,以及「The Visual Studio Blog」,BlogID 為 2,我們可以預期以下輸出:
In Local:
Found 0: My New Blog with state Added
Found 2: The Visual Studio Blog with state Unchanged
In DbSet query:
Found 1: ADO.NET Blog with state Deleted
Found 2: The Visual Studio Blog with state Unchanged
這說明了三個重點:
- 新部落格「我的新部落格」雖然尚未被儲存到資料庫,但仍被收錄在地方收藏中。 這個部落格的主鍵為零,是因為資料庫尚未產生該實體的真實金鑰。
- 「ADO.NET 部落格」雖仍依上下文追蹤,但未被納入當地收藏。 這是因為我們從 DbSet 中移除了它,因此將其標記為已刪除。
- 當使用 DbSet 執行查詢時,標記為刪除的部落格(ADO.NET Blog)會被包含在結果中,而尚未儲存到資料庫的新部落格(My New Blog)則不會被包含在結果中。 這是因為 DbSet 正在對資料庫執行查詢,而回傳的結果總是反映資料庫中的內容。
使用 Local 來新增或移除上下文中的實體
DbSet 的 Local 屬性會回傳一個 ObservableCollection ,事件已連接,使其與上下文內容保持同步。 這表示實體可以從本地集合或資料庫集合中新增或移除。 這也代表將新實體帶入上下文的查詢,會導致更新本地集合,以包含這些實體。 例如:
using (var context = new BloggingContext())
{
// Load some posts from the database into the context
context.Posts.Where(p => p.Tags.Contains("entity-framework")).Load();
// Get the local collection and make some changes to it
var localPosts = context.Posts.Local;
localPosts.Add(new Post { Name = "What's New in EF" });
localPosts.Remove(context.Posts.Find(1));
// Loop over the posts in the context.
Console.WriteLine("In Local after entity-framework query: ");
foreach (var post in context.Posts.Local)
{
Console.WriteLine(
"Found {0}: {1} with state {2}",
post.Id,
post.Title,
context.Entry(post).State);
}
var post1 = context.Posts.Find(1);
Console.WriteLine(
"State of post 1: {0} is {1}",
post1.Name,
context.Entry(post1).State);
// Query some more posts from the database
context.Posts.Where(p => p.Tags.Contains("asp.net")).Load();
// Loop over the posts in the context again.
Console.WriteLine("\nIn Local after asp.net query: ");
foreach (var post in context.Posts.Local)
{
Console.WriteLine(
"Found {0}: {1} with state {2}",
post.Id,
post.Title,
context.Entry(post).State);
}
}
假設我們有幾篇貼文標註了「entity-framework」和「asp.net」,輸出可能會是這樣:
In Local after entity-framework query:
Found 3: EF Designer Basics with state Unchanged
Found 5: EF Code First Basics with state Unchanged
Found 0: What's New in EF with state Added
State of post 1: EF Beginners Guide is Deleted
In Local after asp.net query:
Found 3: EF Designer Basics with state Unchanged
Found 5: EF Code First Basics with state Unchanged
Found 0: What's New in EF with state Added
Found 4: ASP.NET Beginners Guide with state Unchanged
這說明了三個重點:
- 新增至本機集合的「EF 新內容」貼文會在「新增」狀態下被上下文追蹤。 因此,當呼叫 SaveChanges 時,該資料會入資料庫中。
- 從本地收藏(EF 初學者指南)中被移除的貼文,現在在上下文中被標記為已刪除。 因此,當呼叫 SaveChanges 時,該資料會從資料庫中刪除。
- 附加的文章(ASP.NET 初學者指南)會隨著第二個查詢自動加入本地收藏。
關於 Local,最後要注意的是,由於它是 ObservableCollection,所以對於大量實體來說,效能並不理想。 因此,如果你在情境中處理成千上萬個實體,可能不建議使用 Local。
使用 Local 進行 WPF 資料綁定
DbSet 上的 Local 屬性可直接用於 WPF 應用程式中的資料綁定,因為它是 ObservableCollection 的實例。 如前述所述,這表示它會自動與上下文內容同步,而上下文的內容也會自動與上下文同步。 請注意,你需要預先填入本地集合的資料,才能有東西可以綁定,因為本地不會引發資料庫查詢。
這裡並非完整 WPF 資料綁定範例的適當地點,但關鍵元素如下:
- 建立一個綁定來源
- 將其綁定到您集合的 Local 屬性
- 透過查詢資料庫來更新本地資料。
WPF 綁定至導航屬性
如果你在做主資料/詳細資料綁定,建議將詳細視圖綁定到你某個實體的導航屬性。 一個簡單的方法是使用 ObservableCollection 作為導航屬性。 例如:
public class Blog
{
private readonly ObservableCollection<Post> _posts =
new ObservableCollection<Post>();
public int BlogId { get; set; }
public string Name { get; set; }
public virtual ObservableCollection<Post> Posts
{
get { return _posts; }
}
}
使用 Local 來清理 SaveChanges 中的實體
在大多數情況下,從導航屬性移除的實體不會在上下文中自動標記為已刪除。 例如,如果你從 Blog.Posts 集合中移除一個 Post 物件,那麼當呼叫 SaveChanges 時,該文章不會自動被刪除。 如果你需要刪除它,可能需要先找到這些懸掛的實體,並在呼叫 SaveChanges 或作為覆寫 SaveChanges 的一部分前,先標記它們為已刪除。 例如:
public override int SaveChanges()
{
foreach (var post in this.Posts.Local.ToList())
{
if (post.Blog == null)
{
this.Posts.Remove(post);
}
}
return base.SaveChanges();
}
上述程式碼使用本地集合來尋找所有文章,並將沒有部落格參考的文章標記為已刪除。 ToList 呼叫是必要的,否則在列舉過程中,Remove 方法呼叫會修改集合。 在大多數其他情況下,你可以直接查詢本地屬性,不用先用 ToList。
使用 Local 與 ToBindingList 進行 Windows 表單資料綁定
Windows Forms 不支援直接使用 ObservableCollection 進行完整資料綁定。 不過,你仍然可以使用 DbSet Local 屬性來綁定資料,以獲得前面描述的所有好處。 這是透過 ToBindingList 擴充方法實現的,該方法建立一個由 Local ObservableCollection 支援的 IBindingList 實作。
這裡不是完整 Windows Forms 資料綁定範例的合適地方,但關鍵元素如下:
- 建立物件綁定原始碼
- 用 Local.ToBindingList() 把它綁定到你集合的 Local 屬性
- 利用查詢資料庫填充本地
獲取被追蹤實體的詳細資訊
本系列中的許多範例使用 Entry 方法來回傳一個實體的 DbEntityEntry 實例。 這個入口物件接著作為起點,用來收集關於該實體的資訊,例如其當前狀態,以及執行相關操作,例如明確載入相關實體。
Entries 方法會回傳許多或所有被此上下文追蹤的實體的 DbEntityEntry 物件。 這讓你能蒐集資訊或對多個實體執行操作,而不只是單一條目。 例如:
using (var context = new BloggingContext())
{
// Load some entities into the context
context.Blogs.Load();
context.Authors.Load();
context.Readers.Load();
// Make some changes
context.Blogs.Find(1).Title = "The New ADO.NET Blog";
context.Blogs.Remove(context.Blogs.Find(2));
context.Authors.Add(new Author { Name = "Jane Doe" });
context.Readers.Find(1).Username = "johndoe1987";
// Look at the state of all entities in the context
Console.WriteLine("All tracked entities: ");
foreach (var entry in context.ChangeTracker.Entries())
{
Console.WriteLine(
"Found entity of type {0} with state {1}",
ObjectContext.GetObjectType(entry.Entity.GetType()).Name,
entry.State);
}
// Find modified entities of any type
Console.WriteLine("\nAll modified entities: ");
foreach (var entry in context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified))
{
Console.WriteLine(
"Found entity of type {0} with state {1}",
ObjectContext.GetObjectType(entry.Entity.GetType()).Name,
entry.State);
}
// Get some information about just the tracked blogs
Console.WriteLine("\nTracked blogs: ");
foreach (var entry in context.ChangeTracker.Entries<Blog>())
{
Console.WriteLine(
"Found Blog {0}: {1} with original Name {2}",
entry.Entity.BlogId,
entry.Entity.Name,
entry.Property(p => p.Name).OriginalValue);
}
// Find all people (author or reader)
Console.WriteLine("\nPeople: ");
foreach (var entry in context.ChangeTracker.Entries<IPerson>())
{
Console.WriteLine("Found Person {0}", entry.Entity.Name);
}
}
你會注意到我們在範例中引入了 Author 和 Reader 類別——這兩個類別都實作了 IPerson 介面。
public class Author : IPerson
{
public int AuthorId { get; set; }
public string Name { get; set; }
public string Biography { get; set; }
}
public class Reader : IPerson
{
public int ReaderId { get; set; }
public string Name { get; set; }
public string Username { get; set; }
}
public interface IPerson
{
string Name { get; }
}
假設資料庫中有以下資料:
部落格 BlogID = 1,名稱 = 『ADO.NET 部落格』
部落格 BlogID = 2,名稱 = 『Visual Studio 部落格』
具有 BlogId = 3 且名稱 = '.NET Framework Blog' 的「部落格」
作者,AuthorID = 1,姓名 = 'Joe Bloggs'
讀取器,ReaderID = 1,名稱 = 「John Doe」
執行程式碼的輸出會是:
All tracked entities:
Found entity of type Blog with state Modified
Found entity of type Blog with state Deleted
Found entity of type Blog with state Unchanged
Found entity of type Author with state Unchanged
Found entity of type Author with state Added
Found entity of type Reader with state Modified
All modified entities:
Found entity of type Blog with state Modified
Found entity of type Reader with state Modified
Tracked blogs:
Found Blog 1: The New ADO.NET Blog with original Name ADO.NET Blog
Found Blog 2: The Visual Studio Blog with original Name The Visual Studio Blog
Found Blog 3: .NET Framework Blog with original Name .NET Framework Blog
People:
Found Person John Doe
Found Person Joe Bloggs
Found Person Jane Doe
這些例子說明了幾個重點:
- Entry 方法會回傳所有狀態的實體項目,包括已刪除狀態。 與排除已刪除實體的本地相比。
- 當使用非通用條目方法時,所有實體類型的條目都會回傳。 當使用通用條目方法時,只有屬於該通用型別的實體會回傳條目。 上面用這個方法來取得所有部落格的條目。 它也被用來取得所有實作 IPerson 實體的條目。 這顯示通用型別不必是實際的實體型別。
- LINQ 轉物件可用來過濾回傳的結果。 上述方法用來尋找任何類型的實體,只要它們被修改過即可。
請注意,DbEntityEntry 實例總是包含非空實體。 關係項目與存根項目不會以 DbEntityEntry 實例表示,因此無需篩選。