共用方式為


其他變更追蹤功能

本檔涵蓋涉及變更追蹤的雜項功能和案例。

提示

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

提示

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

AddAddAsync

Entity Framework Core (EF Core) 每當使用該方法可能會導致資料庫互動時,都提供非同步方法。 使用不支援高效能非同步存取的資料庫時,也會提供同步方法以避免額外負荷。

DbContext.AddDbSet<TEntity>.Add 通常不會存取資料庫,因為這些方法原本就只是開始追蹤實體。 不過,某些形式的值產生 可能會 存取資料庫,以產生索引鍵值。 執行這項動作並隨附于 EF Core 的唯一值產生器是 HiLoValueGenerator<TValue> 。 使用此產生器並不常見;它預設永遠不會設定。 這表示絕大多數應用程式都應該使用 Add ,而不是 AddAsync

其他類似的方法,例如 UpdateAttachRemove 沒有非同步多載,因為它們永遠不會產生新的索引鍵值,因此永遠不需要存取資料庫。

AddRangeUpdateRangeAttachRangeRemoveRange

DbSet<TEntity>DbContext 提供 、 UpdateAttachRemoveAdd 替代版本,以接受單一呼叫中的多個實例。 這些方法分別為 AddRangeUpdateRangeAttachRangeRemoveRange

為了方便起見,會提供這些方法。 使用 「range」 方法的功能與對等的非範圍方法的多個呼叫相同。 這兩種方法之間沒有顯著的效能差異。

注意

這與 EF6 不同,其中 AddRangeAdd 都會自動呼叫 DetectChanges ,但多次呼叫 Add 會導致 DetectChanges 多次呼叫,而不是呼叫一次。 這讓 AddRange EF6 更有效率。 在 EF Core 中,這兩種方法都不會自動呼叫 DetectChanges

DbCoNtext 與 DbSet 方法

許多方法,包括 AddAttach Update 和 ,在 和 Remove DbContextDbSet<TEntity> 都有 實作。 這些方法對一般實體類型的行為 完全相同 。 這是因為實體的 CLR 類型對應到 EF Core 模型中只有一個實體類型。 因此,CLR 類型會完整定義實體放入模型中的位置,因此可以隱含判斷要使用的 DbSet。

此規則的例外狀況是使用共用類型實體類型,主要用於多對多聯結實體。 使用共用類型實體類型時,必須先針對正在使用的 EF Core 模型類型建立 DbSet。 接著,可以在 DbSet 上使用 、 UpdateAttachRemoveAdd 方法,而不用任何模棱兩可的 EF Core 模型類型。

共用類型實體類型預設會用於多對多關聯性中的聯結實體。 您也可以明確設定共用類型實體類型,以用於多對多關聯性。 例如,下列程式碼會 Dictionary<string, int> 設定為聯結實體類型:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

變更外鍵和導覽 會示範如何藉由追蹤新的聯結實體實例來建立兩個實體的關聯。 下列程式碼會針對 Dictionary<string, int> 用於聯結實體的共用類型實體類型執行此動作:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

請注意, DbContext.Set<TEntity>(String) 用來建立 PostTag 實體類型的 DbSet。 接著,您可以使用這個 DbSet 來呼叫 Add 與新的聯結實體實例。

重要

依慣例用於聯結實體類型的 CLR 類型在未來版本中可能會變更,以改善效能。 除非已在上述程式碼中明確設定為 , Dictionary<string, int> 否則請勿相依于任何特定的聯結實體類型。

屬性與欄位存取

實體屬性的存取預設會使用 屬性的備份欄位。 這是有效率的,可避免從呼叫屬性 getter 和 setter 觸發副作用。 例如,這是延遲載入如何避免觸發無限迴圈。 如需在模型中設定支援欄位的詳細資訊,請參閱 備份欄位

有時候,EF Core 在修改屬性值時可能會產生副作用。 例如,當資料系結至實體時,設定屬性可能會產生通知給 U.I。直接設定欄位時不會發生。 您可以藉由變更 PropertyAccessMode 下列專案的 來達成此目的:

屬性存取模式 Field ,並 PreferField 會導致 EF Core 透過其備份欄位存取屬性值。 同樣地, Property 而且 PreferProperty 會導致 EF Core 透過其 getter 和 setter 存取屬性值。

如果使用 或 Property ,且 Field EF Core 無法透過欄位或屬性 getter/setter 分別存取值,則 EF Core 會擲回例外狀況。 這可確保 EF Core 一律會在您認為時使用欄位/屬性存取。

另一方面,如果無法使用慣用的存取權,和 PreferField PreferProperty 模式將會分別回復為使用 屬性或備份欄位。 PreferField 是預設值。 這表示 EF Core 會在可以時使用欄位,但如果屬性必須透過其 getter 或 setter 進行存取,則不會失敗。

FieldDuringConstruction並將 PreferFieldDuringConstruction EF Core 設定為只在建立實體實例 時使用備份欄位 。 這可讓查詢在沒有 getter 和 setter 副作用的情況下執行,而 EF Core 稍後的屬性變更會導致這些副作用。

下表摘要說明不同的屬性存取模式:

PropertyAccessMode 偏好 建立實體的喜好設定 後援 建立實體的後援
Field 欄位 欄位 擲回 擲回
Property 屬性 屬性 擲回 擲回
PreferField 欄位 欄位 屬性 屬性
PreferProperty 屬性 屬性 欄位 欄位
FieldDuringConstruction 屬性 欄位 欄位 擲回
PreferFieldDuringConstruction 屬性 欄位 欄位 屬性

暫存值

EF Core 會在追蹤在呼叫 SaveChanges 時,資料庫產生實際索引鍵值的新實體時,建立暫存索引鍵值。 如需如何使用這些暫存值的概觀,請參閱 EF Core 中的變更追蹤。

存取暫存值

暫存值會儲存在變更追蹤器中,而不會直接設定為實體實例。 不過,這些暫存值 會在 使用各種機制存取 追蹤實體 時公開。 例如,下列程式碼會使用 EntityEntry.CurrentValues 存取暫存值:

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

此程式碼的輸出如下:

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary 可用來檢查暫存值。

操作暫存值

使用暫存值有時很有用。 例如,可能會在 Web 用戶端上建立新實體的集合,然後序列化回伺服器。 外鍵值是設定這些實體之間關聯性的其中一種方式。 下列程式碼會使用此方法,將新實體的圖表與外鍵產生關聯,同時在呼叫 SaveChanges 時仍允許產生實際索引鍵值。

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -1,
        Title = "Announcing the Release of EF Core 5.0",
        Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
    },
    new Post
    {
        Id = -2,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

請注意:

  • 負數會當做暫存索引鍵值使用;這不是必要的,但是防止主要衝突的常見慣例。
  • Post.BlogIdFK 屬性會指派與相關聯部落格的 PK 相同的負值。
  • PK 值會在追蹤每個實體之後設定 IsTemporary 為暫時性。 這是必要的,因為應用程式提供的任何索引鍵值都假設為實際索引鍵值。

在呼叫 SaveChanges 之前查看 變更追蹤器偵錯檢視 ,顯示 PK 值標示為暫時性,且文章會與正確的部落格相關聯,包括修正流覽:

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  BlogId: -1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -1}

呼叫 SaveChanges 之後,這些暫存值已由資料庫所產生的實際值所取代:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

使用預設值

EF Core 允許在呼叫 時 SaveChanges ,從資料庫取得其預設值。 與產生的索引鍵值一樣,如果尚未明確設定任何值,EF Core 只會使用資料庫中的預設值。 例如,請考慮下列實體類型:

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

屬性 ValidFrom 已設定為從資料庫取得預設值:

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

插入此類型的實體時,除非已設定明確的值,否則 EF Core 會讓資料庫產生值。 例如:

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

查看 變更追蹤器偵錯檢視 會顯示資料庫所產生的第一個權杖 ValidFrom ,而第二個權杖則使用明確設定的值:

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

注意

使用資料庫預設值需要資料庫資料行已設定預設值條件約束。 使用 或 HasDefaultValueHasDefaultValueSql ,EF Core 移轉會自動完成此動作。 當不使用 EF Core 移轉時,請務必以其他方式在資料行上建立預設條件約束。

使用可為 Null 的屬性

EF Core 能夠藉由比較屬性值與該類型的 CLR 預設值,判斷是否已設定屬性。 這在大部分情況下都運作良好,但表示 CLR 預設值無法明確插入資料庫中。 例如,請考慮具有整數屬性的實體:

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

其中該屬性設定為具有 -1 的資料庫預設值:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

其目的是,每當未設定明確值時,就會使用 -1 的預設值。 不過,將值設定為 0(整數的 CLR 預設值)與 EF Core 不設定任何值不區分,這表示無法插入此屬性的 0。 例如:

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

請注意,明確設定為 0 的實例 Count 仍會從資料庫取得預設值,這不是我們預期的情況。 處理此作業的簡單方式是讓 Count 屬性可為 Null:

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

這會使 CLR 預設為 null,而不是 0,這表示現在會在明確設定時插入 0:

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

使用可為 Null 的備份欄位

使屬性成為可為 Null 的問題,在定義域模型中可能不是可為概念上可為 Null。 因此,強制屬性可為 Null 會危害模型。

屬性可以保留為不可為 Null,且只有可 Null 的備份欄位。 例如:

public class Foo3
{
    public int Id { get; set; }

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

如果屬性明確設定為 0,則允許插入 CLR 預設值 (0),而不需要在定義域模型中將屬性公開為可為 Null。 例如:

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

布林屬性的可為 Null 支援欄位

搭配存放區產生的預設值使用 bool 屬性時,此模式特別有用。 由於 的 CLR 預設值 bool 為 「false」,表示無法使用一般模式明確插入 「false」。 例如,請考慮實體 User 類型:

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

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

屬性 IsAuthorized 會設定為 「true」 的資料庫預設值:

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

屬性 IsAuthorized 可以在插入之前明確設定為 「true」 或 「false」,或者可以保留未設定,在此情況下會使用資料庫預設值:

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

context.SaveChanges();

使用 SQLite 時,SaveChanges 的輸出會顯示 Mac 的資料庫預設值,而明確值則設定為 Alice 和 Baxter:

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

僅限架構預設值

有時候,在 EF Core 移轉所建立的資料庫架構中,若未使用這些值進行插入,就很有用。 您可以藉由將 屬性設定為 PropertyBuilder.ValueGeneratedNever ,以達成此目的,例如:

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();