Share via


存取追蹤實體

存取 所 DbContext 追蹤的實體有四個主要 API:

以下各節會更詳細地說明上述各項。

提示

本檔假設瞭解實體狀態和 EF Core 變更追蹤的基本概念。 如需這些主題的詳細資訊,請參閱 EF Core 中的變更追蹤。

提示

您可以從 GitHub 下載範例程式碼,以執行並偵錯此文件中的所有程式碼。

使用 DbCoNtext.Entry 和 EntityEntry 實例

針對每個追蹤的實體,Entity Framework Core (EF Core) 會追蹤:

  • 實體的整體狀態。 這是 、、 或 之 Unchanged 一;如需詳細資訊,請參閱 EF Core 中的 Deleted 變更追蹤。 AddedModified
  • 追蹤實體之間的關聯性。 例如,文章所屬的部落格。
  • 屬性的「目前值」。
  • 當此資訊可供使用時,屬性的「原始值」。 原始值是從資料庫查詢實體時所存在的屬性值。
  • 自從查詢這些屬性值之後,已經修改了哪些屬性值。
  • 屬性值的其他資訊,例如值是否為 暫時性

傳遞實體實例,以 DbContext.Entry 產生 EntityEntry<TEntity> 提供指定實體此資訊的存取權。 例如:

using var context = new BlogsContext();

var blog = context.Blogs.Single(e => e.Id == 1);
var entityEntry = context.Entry(blog);

下列各節說明如何使用 EntityEntry 來存取和操作實體狀態,以及實體屬性和導覽的狀態。

使用實體

最常見的用法 EntityEntry<TEntity> 是存取實體的目前 EntityState 。 例如:

var currentState = context.Entry(blog).State;
if (currentState == EntityState.Unchanged)
{
    context.Entry(blog).State = EntityState.Modified;
}

Entry 方法也可以在尚未追蹤的實體上使用。 這 不會開始追蹤實體 ;實體的狀態仍然是 Detached 。 不過,傳回的 EntityEntry 接著可用來變更實體狀態,此時實體將會在指定的狀態中追蹤。 例如,下列程式碼會開始將部落格實例追蹤為 Added

var newBlog = new Blog();
Debug.Assert(context.Entry(newBlog).State == EntityState.Detached);

context.Entry(newBlog).State = EntityState.Added;
Debug.Assert(context.Entry(newBlog).State == EntityState.Added);

提示

不同于 EF6,設定個別實體的狀態不會造成追蹤所有已連線的實體。 如此一來,設定狀態會比呼叫 、 AttachUpdate ,以這種方式在 Add 實體的整個圖形上運作。

下表摘要說明如何使用 EntityEntry 來處理整個實體的方法:

EntityEntry 成員 描述
EntityEntry.State 取得和設定 EntityState 實體的 。
EntityEntry.Entity 取得實體實例。
EntityEntry.Context DbContext正在追蹤此實體的 。
EntityEntry.Metadata IEntityType 實體類型的中繼資料。
EntityEntry.IsKeySet 實體是否已設定其索引鍵值。
EntityEntry.Reload() 以從資料庫讀取的值覆寫屬性值。
EntityEntry.DetectChanges() 僅強制偵測此實體的變更;請參閱 變更偵測和通知

使用單一屬性

數個 EntityEntry<TEntity>.Property 多載允許存取實體個別屬性的相關資訊。 例如,使用強型別的 fluent-like API:

PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property(e => e.Name);

屬性名稱可以改為以字串的形式傳遞。 例如:

PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property<string>("Name");

然後,傳回的 PropertyEntry<TEntity,TProperty> 可用來存取 屬性的相關資訊。 例如,它可以用來取得並設定此實體上屬性的目前值:

string currentValue = context.Entry(blog).Property(e => e.Name).CurrentValue;
context.Entry(blog).Property(e => e.Name).CurrentValue = "1unicorn2";

上述兩個屬性方法都會傳回強型別泛型 PropertyEntry<TEntity,TProperty> 實例。 使用這個泛型型別是慣用的,因為它允許存取屬性值,而不需要 Boxing 實值型別 。 不過,如果在編譯階段不知道實體或屬性的類型,則可以改為取得非泛型 PropertyEntry

PropertyEntry propertyEntry = context.Entry(blog).Property("Name");

這允許存取任何屬性的屬性資訊,不論其類型為何,都會犧牲 Boxing 實值型別。 例如:

object blog = context.Blogs.Single(e => e.Id == 1);

object currentValue = context.Entry(blog).Property("Name").CurrentValue;
context.Entry(blog).Property("Name").CurrentValue = "1unicorn2";

下表摘要說明 PropertyEntry 所公開的屬性資訊:

PropertyEntry 成員 描述
PropertyEntry<TEntity,TProperty>.CurrentValue 取得和設定 屬性的目前值。
PropertyEntry<TEntity,TProperty>.OriginalValue 取得並設定屬性的原始值,如果有的話。
PropertyEntry<TEntity,TProperty>.EntityEntry 實體的 EntityEntry<TEntity> 返回參考。
PropertyEntry.Metadata IProperty 屬性的中繼資料。
PropertyEntry.IsModified 指出這個屬性是否標示為已修改,並允許變更此狀態。
PropertyEntry.IsTemporary 指出這個屬性是否標示為 暫時性 ,並允許變更此狀態。

注意:

  • 屬性的原始值是屬性從資料庫查詢實體時具有的值。 不過,如果實體已中斷連線,然後明確附加至另一個 DbCoNtext,例如 搭配 AttachUpdate ,則無法使用原始值。 在此情況下,傳回的原始值會與目前值相同。
  • SaveChanges 只會更新標示為已修改的屬性。 設定 IsModified 為 true 以強制 EF Core 更新指定的屬性值,或將它設定為 false,以防止 EF Core 更新屬性值。
  • 暫時值 通常是由 EF Core 值產生器 所產生。 設定屬性的目前值會將暫存值取代為指定的值,並將屬性標示為非暫時值。 設定 IsTemporary 為 true,強制值在明確設定之後為暫時。

使用單一導覽

、 和 EntityEntry.NavigationEntityEntry<TEntity>.ReferenceEntityEntry<TEntity>.Collection 數個多載允許存取個別導覽的相關資訊。

透過 方法存取單一相關實體的 Reference 參考導覽。 參考導覽指向一對多關聯性的「一」端,以及一對一關聯性的兩端。 例如:

ReferenceEntry<Post, Blog> referenceEntry1 = context.Entry(post).Reference(e => e.Blog);
ReferenceEntry<Post, Blog> referenceEntry2 = context.Entry(post).Reference<Blog>("Blog");
ReferenceEntry referenceEntry3 = context.Entry(post).Reference("Blog");

當用於一對多和多對多關聯性的「多對多」端時,流覽也可以是相關實體的集合。 方法 Collection 可用來存取集合導覽。 例如:

CollectionEntry<Blog, Post> collectionEntry1 = context.Entry(blog).Collection(e => e.Posts);
CollectionEntry<Blog, Post> collectionEntry2 = context.Entry(blog).Collection<Post>("Posts");
CollectionEntry collectionEntry3 = context.Entry(blog).Collection("Posts");

某些作業適用于所有導覽。 您可以使用 方法來存取這些參考和集合導覽 EntityEntry.Navigation 。 請注意,一起存取所有導覽時,只能使用非泛型存取。 例如:

NavigationEntry navigationEntry = context.Entry(blog).Navigation("Posts");

下表摘要說明使用 ReferenceEntry<TEntity,TProperty>CollectionEntry<TEntity,TRelatedEntity>NavigationEntry 的方法:

NavigationEntry 成員 描述
MemberEntry.CurrentValue 取得和設定巡覽的目前值。 這是集合導覽的整個集合。
NavigationEntry.Metadata INavigationBase 導覽的中繼資料。
NavigationEntry.IsLoaded 取得或設定值,指出是否已從資料庫完整載入相關的實體或集合。
NavigationEntry.Load() 從資料庫載入相關的實體或集合;請參閱 明確載入相關資料
NavigationEntry.Query() EF Core 查詢會用來將此導覽載入為 IQueryable 可進一步撰寫的 ;請參閱 明確載入相關資料

使用實體的所有屬性

EntityEntry.Properties 針對 IEnumerable<T>PropertyEntry 實體的每個屬性,傳回 的 。 這可用來針對實體的每個屬性執行動作。 例如,若要將任何 DateTime 屬性設定為 DateTime.Now

foreach (var propertyEntry in context.Entry(blog).Properties)
{
    if (propertyEntry.Metadata.ClrType == typeof(DateTime))
    {
        propertyEntry.CurrentValue = DateTime.Now;
    }
}

此外,EntityEntry 也包含數種方法,可同時取得和設定所有屬性值。 這些方法會使用 PropertyValues 類別,代表屬性及其值的集合。 PropertyValues 可以取得目前或原始值,或目前儲存在資料庫中的值。 例如:

var currentValues = context.Entry(blog).CurrentValues;
var originalValues = context.Entry(blog).OriginalValues;
var databaseValues = context.Entry(blog).GetDatabaseValues();

這些 PropertyValues 物件本身並不十分有用。 不過,可以結合它們來執行操作實體時所需的一般作業。 這在處理資料傳輸物件時以及解決 開放式並行衝突 時很有用。 下列各節顯示一些範例。

從實體或 DTO 設定目前或原始值

從另一個物件複製值,即可更新實體的目前或原始值。 例如,請考慮 BlogDto 具有與實體類型相同屬性的資料傳輸物件 (DTO):

public class BlogDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

這可用來使用 PropertyValues.SetValues 來設定追蹤實體的目前值:

var blogDto = new BlogDto { Id = 1, Name = "1unicorn2" };

context.Entry(blog).CurrentValues.SetValues(blogDto);

使用從服務呼叫或多層式應用程式中用戶端取得的值來更新實體時,有時會使用這項技術。 請注意,使用的物件不一定與實體的類型相同,只要其名稱符合實體的屬性。 在上述範例中,會使用 DTO BlogDto 的實例來設定追蹤 Blog 實體的目前值。

請注意,只有在值集與目前值不同時,才會將屬性標示為修改。

設定字典中的目前或原始值

上述範例會設定實體或 DTO 實例的值。 當屬性值儲存為字典中的名稱/值組時,可以使用相同的行為。 例如:

var blogDictionary = new Dictionary<string, object> { ["Id"] = 1, ["Name"] = "1unicorn2" };

context.Entry(blog).CurrentValues.SetValues(blogDictionary);

從資料庫設定目前或原始值

實體的目前或原始值可以使用資料庫中的最新值來更新,方法是呼叫 GetDatabaseValues()GetDatabaseValuesAsync ,以及使用傳回的物件來設定目前或原始值,或兩者。 例如:

var databaseValues = context.Entry(blog).GetDatabaseValues();
context.Entry(blog).CurrentValues.SetValues(databaseValues);
context.Entry(blog).OriginalValues.SetValues(databaseValues);

建立包含目前、原始或資料庫值的複製物件

從 CurrentValues、OriginalValues 或 GetDatabaseValues 傳回的 PropertyValues 物件可用來使用 PropertyValues.ToObject() 建立實體的複製品。 例如:

var clonedBlog = context.Entry(blog).GetDatabaseValues().ToObject();

請注意, ToObject 傳回 DbCoNtext 未追蹤的新實例。 傳回的物件也不會將任何關聯性設定為其他實體。

複製的物件可用於解決與資料庫並行更新相關的問題,特別是當資料系結至特定類型的物件時。 如需詳細資訊,請參閱 開放式並行 存取。

使用實體的所有導覽

EntityEntry.NavigationsIEnumerable<T>NavigationEntry 針對實體的每個導覽傳回 的 。 EntityEntry.ReferencesEntityEntry.Collections 會執行相同動作,但僅限於參考或集合導覽。 這可用來針對實體的每個導覽執行動作。 例如,若要強制載入所有相關實體:

foreach (var navigationEntry in context.Entry(blog).Navigations)
{
    navigationEntry.Load();
}

使用實體的所有成員

一般屬性和導覽屬性有不同的狀態和行為。 因此,處理導覽和非導覽很常見,如上述各節所示。 不過,不論實體是一般屬性還是導覽,有時候對實體的任何成員執行某些動作會很有用。 EntityEntry.MemberEntityEntry.Members 會針對此目的提供。 例如:

foreach (var memberEntry in context.Entry(blog).Members)
{
    Console.WriteLine(
        $"Member {memberEntry.Metadata.Name} is of type {memberEntry.Metadata.ClrType.ShortDisplayName()} and has value {memberEntry.CurrentValue}");
}

在範例的部落格上執行此程式碼會產生下列輸出:

Member Id is of type int and has value 1
Member Name is of type string and has value .NET Blog
Member Posts is of type IList<Post> and has value System.Collections.Generic.List`1[Post]

提示

變更 追蹤器偵錯檢視 會顯示如下的資訊。 整個變更追蹤器的偵錯檢視是從每個追蹤實體的個別 EntityEntry.DebugView 產生。

尋找和 FindAsync

DbContext.FindDbContext.FindAsyncDbSet<TEntity>.FindDbSet<TEntity>.FindAsync 是專為在已知單一實體的主鍵時有效率地查閱所設計。 尋找第一次檢查實體是否已追蹤,如果是,則立即傳回實體。 只有在實體未在本機追蹤時,才會進行資料庫查詢。 例如,請考慮針對相同實體呼叫 Find 兩次的程式碼:

using var context = new BlogsContext();

Console.WriteLine("First call to Find...");
var blog1 = context.Blogs.Find(1);

Console.WriteLine($"...found blog {blog1.Name}");

Console.WriteLine();
Console.WriteLine("Second call to Find...");
var blog2 = context.Blogs.Find(1);
Debug.Assert(blog1 == blog2);

Console.WriteLine("...returned the same instance without executing a query.");

使用 SQLite 時,此程式碼的輸出(包括 EF Core 記錄) 為:

First call to Find...
info: 12/29/2020 07:45:53.682 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@__p_0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
      SELECT "b"."Id", "b"."Name"
      FROM "Blogs" AS "b"
      WHERE "b"."Id" = @__p_0
      LIMIT 1
...found blog .NET Blog

Second call to Find...
...returned the same instance without executing a query.

請注意,第一次呼叫在本機找不到實體,因此會執行資料庫查詢。 相反地,第二個呼叫會傳回相同的實例,而不查詢資料庫,因為它已經追蹤。

如果具有指定索引鍵的實體未在本機追蹤且不存在於資料庫中,則尋找 會傳回 null。

複合索引鍵

Find 也可以搭配複合索引鍵使用。 例如,假設有 OrderLine 複合索引鍵的實體,其中包含訂單識別碼和產品識別碼:

public class OrderLine
{
    public int OrderId { get; set; }
    public int ProductId { get; set; }

    //...
}

複合索引鍵必須設定為 DbContext.OnModelCreating 定義索引鍵部分 及其順序 。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<OrderLine>()
        .HasKey(e => new { e.OrderId, e.ProductId });
}

請注意, OrderId 是索引鍵的第一個部分,而 ProductId 是索引鍵的第二個部分。 將索引鍵值傳遞至 Find 時,必須使用這個順序。 例如:

var orderline = context.OrderLines.Find(orderId, productId);

使用 ChangeTracker.Entries 存取所有追蹤的實體

到目前為止,我們一次只能存取一個 EntityEntryChangeTracker.Entries() 會針對 DbCoNtext 目前追蹤的每個實體,傳回 EntityEntry。 例如:

using var context = new BlogsContext();
var blogs = context.Blogs.Include(e => e.Posts).ToList();

foreach (var entityEntry in context.ChangeTracker.Entries())
{
    Console.WriteLine($"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property("Id").CurrentValue}");
}

此程式碼會產生下列輸出:

Found Blog entity with ID 1
Found Post entity with ID 1
Found Post entity with ID 2

請注意,會傳回部落格和文章的專案。 您可以改用 ChangeTracker.Entries<TEntity>() 泛型多載,將結果篩選為特定實體類型:

foreach (var entityEntry in context.ChangeTracker.Entries<Post>())
{
    Console.WriteLine(
        $"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

此程式碼的輸出會顯示只會傳回貼文:

Found Post entity with ID 1
Found Post entity with ID 2

此外,使用泛型多載會傳回泛型 EntityEntry<TEntity> 實例。 這是允許在此範例中存取 Id 屬性的類似 Fluent。

用於篩選的泛型型別不一定是對應的實體類型;您可以改用未對應的基底類型或介面。 例如,如果模型中的所有實體類型實作定義其索引鍵屬性的介面:

public interface IEntityWithKey
{
    int Id { get; set; }
}

然後,這個介面可以用來以強型別的方式處理任何追蹤實體的索引鍵。 例如:

foreach (var entityEntry in context.ChangeTracker.Entries<IEntityWithKey>())
{
    Console.WriteLine(
        $"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

使用 DbSet.Local 查詢追蹤的實體

EF Core 查詢一律會在資料庫上執行,而且只會傳回已儲存至資料庫的實體。 DbSet<TEntity>.Local 提供機制來查詢 DbCoNtext 以取得本機、追蹤的實體。

由於 DbSet.Local 用來查詢追蹤的實體,因此通常會將實體載入 DbCoNtext,然後使用這些載入的實體。 這特別適用于資料系結,但在其他情況下也很有用。 例如,在下列程式碼中,資料庫會先針對所有部落格和文章進行查詢。 擴充 Load 方法可用來使用內容追蹤的結果來執行此查詢,而不會直接傳回至應用程式。 (使用 ToList 或類似專案的效果相同,但建立傳回清單的額外負荷,這裡不需要此清單。此範例接著會使用 DbSet.Local 來存取本機追蹤的實體:

using var context = new BlogsContext();

context.Blogs.Include(e => e.Posts).Load();

foreach (var blog in context.Blogs.Local)
{
    Console.WriteLine($"Blog: {blog.Name}");
}

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"Post: {post.Title}");
}

請注意,不同于 ChangeTracker.Entries() ,會 DbSet.Local 直接傳回實體實例。 當然,您可以呼叫 DbContext.Entry 來取得傳回實體的 EntityEntry。

本機檢視

DbSet<TEntity>.Local 會傳回反映這些實體目前 EntityState 之本機追蹤實體的檢視。 具體來說,這表示:

  • Added 包含實體。 請注意,這並非一般 EF Core 查詢的情況,因為 Added 資料庫中尚未存在實體,因此不會由資料庫查詢傳回。
  • Deleted 實體會排除。 請注意,這再次不是一般 EF Core 查詢的情況,因為 Deleted 實體仍然存在於資料庫中,因此資料庫查詢會 傳回。

DbSet.Local 表示會檢視反映實體圖形目前概念狀態的資料,其中包含 Added 實體和 Deleted 排除的實體。 這會比對呼叫 SaveChanges 之後預期的資料庫狀態。

這通常是資料系結的理想檢視,因為它會根據應用程式所做的變更,向使用者呈現資料。

下列程式碼將一個貼文標示為 Deleted ,然後新增一個貼文,並將其標示為 ,藉 Added 以示範這一點:

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

Console.WriteLine("Local view after loading posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

context.Remove(posts[1]);

context.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many...",
        Blog = posts[0].Blog
    });

Console.WriteLine("Local view after adding and deleting posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

此程式碼的輸出如下:

Local view after loading posts:
  Post: Announcing the Release of EF Core 5.0
  Post: Announcing F# 5
  Post: Announcing .NET 5.0
Local view after adding and deleting posts:
  Post: What’s next for System.Text.Json?
  Post: Announcing the Release of EF Core 5.0
  Post: Announcing .NET 5.0

請注意,已刪除的貼文會從本機檢視中移除,並包含新增的貼文。

使用 Local 新增和移除實體

DbSet<TEntity>.Local 會傳回 LocalView<TEntity> 的執行個體。 這是 的 ICollection<T> 實作,會在從集合新增和移除實體時產生和回應通知。 (這個概念與 ObservableCollection<T> 相同,但實作為對現有 EF Core 變更追蹤專案的投影,而不是獨立集合。

本機檢視的通知會連結至 DbCoNtext 變更追蹤,讓本機檢視與 DbCoNtext 保持同步。 具體而言:

  • 新增實體以 DbSet.Local 讓 DbCoNtext 追蹤它,通常是處於 Added 狀態。 (如果實體已經有產生的索引鍵值,則會改為追蹤 Unchanged 它。
  • 從 中移除實體 DbSet.Local 會導致實體標示為 Deleted
  • DbCoNtext 所追蹤的實體會自動出現在集合中 DbSet.Local 。 例如,執行查詢以帶入更多實體時,會自動更新本機檢視。
  • 標示為 Deleted 的實體會自動從本機集合中移除。

這表示本機檢視可用來操作追蹤實體,只要從集合新增和移除即可。 例如,讓我們修改先前的範例程式碼,以從本機集合新增和移除貼文:

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

Console.WriteLine("Local view after loading posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

context.Posts.Local.Remove(posts[1]);

context.Posts.Local.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many...",
        Blog = posts[0].Blog
    });

Console.WriteLine("Local view after adding and deleting posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

因為對本機檢視所做的變更會與 DbCoNtext 同步,因此輸出會與上一個範例維持不變。

使用 Windows Forms 或 WPF 資料系結的本機檢視

DbSet<TEntity>.Local 會形成資料系結至 EF Core 實體的基礎。 不過,Windows Forms 和 WPF 在搭配特定類型的通知集合使用時效果最佳。 本機檢視支援建立這些特定的集合類型:

例如:

ObservableCollection<Post> observableCollection = context.Posts.Local.ToObservableCollection();
BindingList<Post> bindingList = context.Posts.Local.ToBindingList();

如需使用 EF Core 進行 WPF 資料系結的詳細資訊,請參閱 開始使用 WPF 資料系結和開始使用 Windows Forms 以取得使用 EF Core 進行 Windows Forms 資料系結的詳細資訊。

提示

第一次存取並快取時,會延遲建立指定 DbSet 實例的本機檢視。 LocalView 建立本身的速度很快,而且不會使用大量的記憶體。 不過,它會呼叫 DetectChanges ,這對大量實體而言可能很慢。 和 ToBindingListToObservableCollection 建立的集合也會延遲建立,然後快取。 這兩種方法都會建立新的集合,當涉及數千個實體時,可能會變慢並使用大量的記憶體。