添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

您可以 從 GitHub 下載範例程式碼 來執行和偵錯範例。 每個區段都會連結到該區段專屬的原始程式碼。

EF8 需要 .NET 8 SDK 才能建置,而且需要 .NET 8 運行時間才能執行。 EF8 不會在舊版 .NET 上執行,也不會在 .NET Framework 上執行。

使用複雜型別的值物件

儲存至資料庫的物件可以分割成三大類別:

  • 非結構化物件並保留單一值。 例如,、 int Guid string IPAddress 。 這些稱為「基本類型」(有點鬆散)。
  • 結構化來保存多個值的物件,以及對象的識別是由索引鍵值所定義的位置。 例如, Blog Post Customer 。 這些稱為「實體類型」。
  • 結構化來保存多個值的物件,但對象沒有定義識別的索引鍵。 例如, Address Coordinate
  • 在 EF8 之前,無法對應第三種類型的物件。 可以使用擁有的類型,但因為擁有的類型 實際上是實體類型,所以即使隱藏了該索引鍵值,它們也有以索引鍵值為基礎的語意。

    EF8 現在支援「複雜類型」,以涵蓋這個第三種類型的物件。 複雜類型物件:

  • 索引鍵值無法識別或追蹤。
  • 必須定義為實體類型的一部分。 (換句話說,您不能有 DbSet 複雜類型的 。
  • 可以是 .NET 實值型 別或 參考型別
  • 實例可以由多個屬性共用。
  • 例如,請考慮類型 Address

    public class Address
        public required string Line1 { get; set; }
        public string? Line2 { get; set; }
        public required string City { get; set; }
        public required string Country { get; set; }
        public required string PostCode { get; set; }
                  Address 然後在簡單的客戶/訂單模型中,在三個地方使用:

    public class Customer
        public int Id { get; set; }
        public required string Name { get; set; }
        public required Address Address { get; set; }
        public List<Order> Orders { get; } = new();
    public class Order
        public int Id { get; set; }
        public required string Contents { get; set; }
        public required Address ShippingAddress { get; set; }
        public required Address BillingAddress { get; set; }
        public Customer Customer { get; set; } = null!;
    

    讓我們使用其位址來建立並儲存客戶:

    var customer = new Customer
        Name = "Willow",
        Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter", Country = "UK", PostCode = "PE14 7AV" }
    context.Add(customer);
    await context.SaveChangesAsync();
    

    這會導致下列資料列插入資料庫中:

    INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
    OUTPUT INSERTED.[Id]
    VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
    

    請注意,複雜型別不會取得自己的數據表。 相反地,它們會內嵌儲存至數據表的數據 Customers 行。 這符合擁有類型的數據表共享行為。

    我們並不打算允許複雜型別對應至自己的數據表。 不過,在未來版本中,我們確實打算允許將複雜類型儲存為單一數據行中的 JSON 檔。 如果問題 #31252 對很重要,請投票給您。

    現在,假設我們想要將訂單寄送給客戶,並使用客戶的位址作為出貨位址的預設帳單。 這樣做的自然方式是將 物件從 AddressCustomer複製到 Order 。 例如:

    customer.Orders.Add(
        new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, });
    await context.SaveChangesAsync();
    

    使用複雜類型時,這會如預期般運作,並將位址插入 Orders 數據表中:

    INSERT INTO [Orders] ([Contents], [CustomerId],
        [BillingAddress_City], [BillingAddress_Country], [BillingAddress_Line1], [BillingAddress_Line2], [BillingAddress_PostCode],
        [ShippingAddress_City], [ShippingAddress_Country], [ShippingAddress_Line1], [ShippingAddress_Line2], [ShippingAddress_PostCode])
    OUTPUT INSERTED.[Id]
    VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);
    

    到目前為止,你可能會說,「但我可以用擁有的類型做到這一點!不過,擁有型別的「實體類型」語意很快就會妨礙。 例如,以擁有的類型執行上述程式代碼會導致大量警告,然後產生錯誤:

    warn: 8/20/2023 12:48:01.678 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) 
          The same entity is being tracked as different entity types 'Order.BillingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
    warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) 
          The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
    warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update)
          The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Order.BillingAddress#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome.
    fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update) 
          An exception occurred in the database while saving changes for context type 'NewInEfCore8.ComplexTypesSample+CustomerContext'.
          System.InvalidOperationException: Cannot save instance of 'Order.ShippingAddress#Address' because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity.
             at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
    

    這是因為實體類型的單一實例 Address (具有相同的隱藏索引鍵值)正用於三 個不同的 實體實例。 另一方面,允許在複雜屬性之間共用相同的實例,因此當使用複雜類型時,程式代碼會如預期般運作。

    複雜類型的設定

    複雜型別必須在模型中使用對應屬性 依慣例不會探索複雜型別。

    例如, Address 可以使用 來設定 ComplexTypeAttribute類型:

    [ComplexType]
    public class Address
        public required string Line1 { get; set; }
        public string? Line2 { get; set; }
        public required string City { get; set; }
        public required string Country { get; set; }
        public required string PostCode { get; set; }
    

    或在 中 OnModelCreating

    protected override void OnModelCreating(ModelBuilder modelBuilder)
        modelBuilder.Entity<Customer>()
            .ComplexProperty(e => e.Address);
        modelBuilder.Entity<Order>(b =>
            b.ComplexProperty(e => e.BillingAddress);
            b.ComplexProperty(e => e.ShippingAddress);
    

    在上述範例中,我們最終使用了三個位置所使用的相同 Address 實例。 這是允許的,而且在使用複雜類型時不會對EF Core造成任何問題。 不過,共用相同參考類型的實例表示,如果修改實例上的屬性值,則該變更將會反映在這三種用法中。 例如,遵循上述內容,讓我們變更 Line1 客戶位址並儲存變更:

    customer.Address.Line1 = "Peacock Lodge";
    await context.SaveChangesAsync();
    

    這會導致使用 SQL Server 時,對資料庫進行下列更新:

    UPDATE [Customers] SET [Address_Line1] = @p0
    OUTPUT 1
    WHERE [Id] = @p1;
    UPDATE [Orders] SET [BillingAddress_Line1] = @p2, [ShippingAddress_Line1] = @p3
    OUTPUT 1
    WHERE [Id] = @p4;
    

    請注意,這三 Line1 個數據行都已變更,因為它們全都共用相同的實例。 這通常不是我們想要的。

    如果客戶位址變更時訂單位址應該自動變更,請考慮將位址對應為實體類型。 Order Customer然後,可以透過導覽屬性安全地參考相同的位址實例(現在由索引鍵識別)。

    處理這類問題的好方法,就是讓類型不可變。 事實上,當類型是複雜類型的好候選專案時,這種不變性通常很自然。 例如,通常提供複雜的新 Address 物件,而不是只改變國家/地區,同時讓其餘部分保持不變是合理的。

    參考和實值型別都可以不可變。 我們將在下列各節中查看一些範例。

    將型別參考為複雜型別

    不可變類別

    我們在上述範例中使用了簡單且可變的 class 。 為了防止上述意外突變的問題,我們可以讓 類別不可變。 例如:

    public class Address
        public Address(string line1, string? line2, string city, string country, string postCode)
            Line1 = line1;
            Line2 = line2;
            City = city;
            Country = country;
            PostCode = postCode;
        public string Line1 { get; }
        public string? Line2 { get; }
        public string City { get; }
        public string Country { get; }
        public string PostCode { get; }
    

    使用 C# 12 或更新版本時,可以使用主要建構函式來簡化此類別定義:

    public class Address(string line1, string? line2, string city, string country, string postCode)
        public string Line1 { get; } = line1;
        public string? Line2 { get; } = line2;
        public string City { get; } = city;
        public string Country { get; } = country;
        public string PostCode { get; } = postCode;
    

    現在無法變更 Line1 現有位址上的值。 相反地,我們需要建立具有已變更值的新實例。 例如:

    var currentAddress = customer.Address;
    customer.Address = new Address(
        "Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
    await context.SaveChangesAsync();
    

    這次呼叫 SaveChangesAsync 只會更新客戶位址:

    UPDATE [Customers] SET [Address_Line1] = @p0
    OUTPUT 1
    WHERE [Id] = @p1;
    

    請注意,即使 Address 物件不可變,而且整個物件已變更,EF 仍會追蹤個別屬性的變更,因此只會更新具有變更值的數據行。

    不可變的記錄

    C# 9 引進 了記錄類型,可讓建立和使用不可變的物件變得更容易。 例如, Address 物件可以建立記錄類型:

    public record Address
        public Address(string line1, string? line2, string city, string country, string postCode)
            Line1 = line1;
            Line2 = line2;
            City = city;
            Country = country;
            PostCode = postCode;
        public string Line1 { get; init; }
        public string? Line2 { get; init; }
        public string City { get; init; }
        public string Country { get; init; }
        public string PostCode { get; init; }
    

    您可以使用主要建構函式來簡化此記錄定義:

    public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
    

    取代可變物件並呼叫 SaveChanges 現在需要較少的程式代碼:

    customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
    await context.SaveChangesAsync();
    

    實值型別做為複雜型別

    簡單的可變 實值型 別可用來做為複雜型別。 例如, Address 可以在 C# 中定義為 struct

    public struct Address
        public required string Line1 { get; set; }
        public string? Line2 { get; set; }
        public required string City { get; set; }
        public required string Country { get; set; }
        public required string PostCode { get; set; }
    

    將客戶 Address 物件指派給出貨和計費 Address 屬性會導致每個屬性取得 的 Address複本,因為這是實值類型的運作方式。 這表示修改 Address 客戶上的 不會變更出貨或計費 Address 實例,因此可變結構不會有與可變動類別發生的相同實例共享問題。

    不過, C# 通常不建議使用可變結構,因此請在使用結構之前仔細思考。

    不可變結構

    不可變結構的運作方式與複雜類型相同,就像不可變的類別一樣。 例如, Address 可以定義,使其無法修改:

    public readonly struct Address(string line1, string? line2, string city, string country, string postCode)
        public string Line1 { get; } = line1;
        public string? Line2 { get; } = line2;
        public string City { get; } = city;
        public string Country { get; } = country;
        public string PostCode { get; } = postCode;
    

    變更位址的程式代碼現在看起來與使用不可變類別時相同:

    var currentAddress = customer.Address;
    customer.Address = new Address(
        "Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode);
    await context.SaveChangesAsync();
    

    不可變的結構記錄

    C# 10 引進 struct record 的類型,可讓您輕鬆地建立和使用不可變的結構記錄,就像是使用不可變的類別記錄一樣。 例如,我們可以將 定義為 Address 不可變的結構記錄:

    public readonly record struct Address(string Line1, string? Line2, string City, string Country, string PostCode);
    

    變更位址的程式代碼現在看起來與使用不可變類別記錄時相同:

    customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
    await context.SaveChangesAsync();
    

    巢狀複雜類型

    複雜類型可以包含其他複雜類型的屬性。 例如,讓我們將 Address 上述的複雜類型與複雜類型一起使用,並將兩者巢狀於另一個 PhoneNumber 複雜類型內:

    public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
    public record PhoneNumber(int CountryCode, long Number);
    public record Contact
        public required Address Address { get; init; }
        public required PhoneNumber HomePhone { get; init; }
        public required PhoneNumber WorkPhone { get; init; }
        public required PhoneNumber MobilePhone { get; init; }
    

    我們在這裡使用不可變的記錄,因為這些是適合複雜型別語意的相符專案,但複雜類型的巢狀化可以使用任何 .NET 類型的類別來完成。

    我們不會針對 Contact 類型使用主要建構函式,因為 EF Core 尚不支援複雜類型值的建構函式插入。 如果這對您很重要,請投票給 問題 #31621

    我們會將 新增 Contact 為 的 Customer屬性:

    public class Customer
        public int Id { get; set; }
        public required string Name { get; set; }
        public required Contact Contact { get; set; }
        public List<Order> Orders { get; } = new();
    

    PhoneNumber 作為屬性 Order

    public class Order
        public int Id { get; set; }
        public required string Contents { get; set; }
        public required PhoneNumber ContactPhone { get; set; }
        public required Address ShippingAddress { get; set; }
        public required Address BillingAddress { get; set; }
        public Customer Customer { get; set; } = null!;
    

    您可以再次使用 ComplexTypeAttribute來設定巢狀複雜型別:

    [ComplexType]
    public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
    [ComplexType]
    public record PhoneNumber(int CountryCode, long Number);
    [ComplexType]
    public record Contact
        public required Address Address { get; init; }
        public required PhoneNumber HomePhone { get; init; }
        public required PhoneNumber WorkPhone { get; init; }
        public required PhoneNumber MobilePhone { get; init; }
    

    或在 中 OnModelCreating

    protected override void OnModelCreating(ModelBuilder modelBuilder)
        modelBuilder.Entity<Customer>(
                b.ComplexProperty(
                    e => e.Contact,
                        b.ComplexProperty(e => e.Address);
                        b.ComplexProperty(e => e.HomePhone);
                        b.ComplexProperty(e => e.WorkPhone);
                        b.ComplexProperty(e => e.MobilePhone);
        modelBuilder.Entity<Order>(
                b.ComplexProperty(e => e.ContactPhone);
                b.ComplexProperty(e => e.BillingAddress);
                b.ComplexProperty(e => e.ShippingAddress);
    

    實體類型上複雜型別的屬性會被視為實體類型的任何其他非導覽屬性。 這表示載入實體類型時,一律會載入它們。 這也適用於任何巢狀複雜類型屬性。 例如,查詢客戶:

    var customer = await context.Customers.FirstAsync(e => e.Id == customerId);
    

    使用 SQL Server 時,會轉譯成下列 SQL:

    SELECT TOP(1) [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country],
        [c].[Contact_Address_Line1], [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode],
        [c].[Contact_HomePhone_CountryCode], [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode],
        [c].[Contact_MobilePhone_Number], [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
    FROM [Customers] AS [c]
    WHERE [c].[Id] = @__customerId_0
    

    請注意此 SQL 的兩件事:

  • 所有項目都會傳回,以填入客戶 所有巢狀 ContactAddressPhoneNumber 複雜類型。
  • 所有複雜類型值都會儲存為實體類型數據表中的數據行。 複雜型別永遠不會對應至個別的數據表。
  • 複雜類型可以從查詢投影。 例如,只從訂單選取出貨位址:

    var shippingAddress = await context.Orders
        .Where(e => e.Id == orderId)
        .Select(e => e.ShippingAddress)
        .SingleAsync();
    

    使用 SQL Server 時,這會轉譯為下列內容:

    SELECT TOP(2) [o].[ShippingAddress_City], [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1],
        [o].[ShippingAddress_Line2], [o].[ShippingAddress_PostCode]
    FROM [Orders] AS [o]
    WHERE [o].[Id] = @__orderId_0
    

    請注意,無法追蹤複雜類型的投影,因為複雜類型對象沒有用於追蹤的身分識別。

    在述詞中使用

    複雜型別的成員可用於述詞中。 例如,尋找前往特定城市的所有訂單:

    var city = "Walpole St Peter";
    var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City == city).ToListAsync();
    

    這會轉譯為 SQL Server 上的下列 SQL:

    SELECT [o].[Id], [o].[Contents], [o].[CustomerId], [o].[BillingAddress_City], [o].[BillingAddress_Country],
        [o].[BillingAddress_Line1], [o].[BillingAddress_Line2], [o].[BillingAddress_PostCode],
        [o].[ContactPhone_CountryCode], [o].[ContactPhone_Number], [o].[ShippingAddress_City],
        [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], [o].[ShippingAddress_Line2],
        [o].[ShippingAddress_PostCode]
    FROM [Orders] AS [o]
    WHERE [o].[ShippingAddress_City] = @__city_0
    

    完整複雜型別實例也可用於述詞中。 例如,尋找具有指定電話號碼的所有客戶:

    var phoneNumber = new PhoneNumber(44, 7777555777);
    var customersWithNumber = await context.Customers
        .Where(
            e => e.Contact.MobilePhone == phoneNumber
                 || e.Contact.WorkPhone == phoneNumber
                 || e.Contact.HomePhone == phoneNumber)
        .ToListAsync();
    

    這會在使用 SQL Server 時轉譯為下列 SQL:

    SELECT [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country], [c].[Contact_Address_Line1],
         [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], [c].[Contact_HomePhone_CountryCode],
         [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode], [c].[Contact_MobilePhone_Number],
         [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
    FROM [Customers] AS [c]
    WHERE ([c].[Contact_MobilePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
        AND [c].[Contact_MobilePhone_Number] = @__entity_equality_phoneNumber_0_Number)
    OR ([c].[Contact_WorkPhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
        AND [c].[Contact_WorkPhone_Number] = @__entity_equality_phoneNumber_0_Number)
    OR ([c].[Contact_HomePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode
        AND [c].[Contact_HomePhone_Number] = @__entity_equality_phoneNumber_0_Number)
    

    請注意,相等是藉由展開複雜類型的每個成員來執行。 這與沒有身分識別索引鍵的複雜型別一致,因此複雜型別實例只有在所有成員都相等時,才會等於另一個複雜類型實例。 這也符合 .NET 針對記錄類型所定義的相等性。

    操作複雜類型值

    EF8 可讓您存取追蹤資訊,例如複雜型別的目前和原始值,以及是否已修改屬性值。 API 複雜類型是已用於實體類型之變更追蹤 API 的延伸模組。

    ComplexProperty 回整個複雜物件之專案的方法 EntityEntry 。 例如,若要取得 的目前值 Order.BillingAddress

    var billingAddress = context.Entry(order)
        .ComplexProperty(e => e.BillingAddress)
        .CurrentValue;
    

    您可以新增 對 Property 的呼叫,以存取複雜類型的屬性。 例如,只取得帳單後代碼的目前值:

    var postCode = context.Entry(order)
        .ComplexProperty(e => e.BillingAddress)
        .Property(e => e.PostCode)
        .CurrentValue;
    

    巢狀複雜類型是使用 對 ComplexProperty的巢狀呼叫來存取。 例如,若要從 上的Address巢狀 ContactCustomer 取得城市:

    var currentCity = context.Entry(customer)
        .ComplexProperty(e => e.Contact)
        .ComplexProperty(e => e.Address)
        .Property(e => e.City)
        .CurrentValue;
    

    其他方法可用於讀取和變更狀態。 例如, PropertyEntry.IsModified 可以用來將複雜類型的屬性設定為已修改:

    context.Entry(customer)
        .ComplexProperty(e => e.Contact)
        .ComplexProperty(e => e.Address)
        .Property(e => e.PostCode)
        .IsModified = true;
    

    目前的限制

    複雜類型代表跨EF堆疊進行大量投資。 我們無法讓此版本的所有專案都能夠運作,但我們計劃在未來版本中關閉一些差距。 修正上述任何限制對於您而言很重要,請務必對適當的 GitHub 問題進行投票👍。

    EF8 中的複雜類型限制包括:

  • 支援複雜類型的集合。 (問題 #31237
  • 允許複雜類型屬性為 Null。 (問題 #31376
  • 將複雜類型屬性對應至 JSON 資料行。 (問題 #31252
  • 複雜類型的建構函式插入。 (問題 #31621
  • 新增複雜類型的種子數據支援。 (問題 #31254
  • 對應 Cosmos 提供者的複雜類型屬性。 (問題 #31253
  • 實作記憶體內部資料庫的複雜類型。 (問題 #31464
  • 使用關係資料庫時的持續性問題,就是使用基本類型集合時要執行的動作;也就是整數、日期/時間、字串等等的清單或陣列。 如果您使用PostgreSQL,則很容易使用PostgreSQL的 內建數位型態來儲存這些專案。 對於其他資料庫,有兩種常見的方法:

  • 使用基本類型值的數據行和另一個數據行建立數據表,以做為外鍵,將每個值連結到集合的擁有者。
  • 將基本集合串行化為資料庫所處理的某些數據行類型,例如,串行化至字串或從字串串列化。
  • 第一個選項在許多情況下都有優點,我們將在本節結尾快速查看。 不過,它不是模型中數據的自然表示法,而且如果您真正擁有的是基本類型的集合,則第二個選項會更有效率。

    從 Preview 4 開始,EF8 現在包含第二個選項的內建支援,使用 JSON 作為串行化格式。 因為新式關係資料庫包含查詢和操作 JSON 的內建機制,因此 JSON 數據行可以視需要有效地視為數據表,而不需要實際建立該數據表的額外負荷。 這些相同的機制可讓 JSON 傳入參數,然後在查詢中以類似方式使用數據表值參數,稍後再進行此動作。

    此處所示的程式碼來自 PrimitiveCollectionsSample.cs

    基本集合屬性

    EF Core 可以將任何 IEnumerable<T> 屬性,其中 T 是基本類型,對應至資料庫中的 JSON 數據行。 這是藉由具有 getter 和 setter 之公用屬性的慣例來完成。 例如,下列實體類型中的所有屬性都會依慣例對應至 JSON 數據行:

    public class PrimitiveCollections
        public IEnumerable<int> Ints { get; set; }
        public ICollection<string> Strings { get; set; }
        public IList<DateOnly> Dates { get; set; }
        public uint[] UnsignedInts { get; set; }
        public List<bool> Booleans { get; set; }
        public List<Uri> Urls { get; set; }
    

    在此內容中,我們所說的「基本類型」是什麼意思? 基本上,資料庫提供者知道如何對應,必要時使用某種值轉換。 例如,在上述實體類型中,類型 intstringDateTimeDateOnlybool 全都會由資料庫提供者處理,而不會進行轉換。 SQL Server 沒有未簽署的 INT 或 URI 的原生支援,但仍uintUri會被視為基本類型,因為這些型別有內建的值轉換器

    根據預設,EF Core 會使用不受限制的 Unicode 字串數據行類型來保存 JSON,因為這可防止大型集合的數據遺失。 不過,在某些資料庫系統上,例如 SQL Server,指定字串的最大長度可以改善效能。 這與其他數據行組態一起,可以正常方式完成。 例如:

    modelBuilder
        .Entity<PrimitiveCollections>()
        .Property(e => e.Booleans)
        .HasMaxLength(1024)
        .IsUnicode(false);
    

    或者,使用對應屬性:

    [MaxLength(2500)]
    [Unicode(false)]
    public uint[] UnsignedInts { get; set; }
    

    默認數據行組態可用於使用 預先慣例模型組態的特定類型所有屬性。 例如:

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
        configurationBuilder
            .Properties<List<DateOnly>>()
            .AreUnicode(false)
            .HaveMaxLength(4000);
    

    具有基本集合的查詢

    讓我們看看一些使用基本類型集合的查詢。 為此,我們需要具有兩個實體類型的簡單模型。 第一個 代表英國公共住宅或「酒吧」:

    public class Pub
        public Pub(string name, string[] beers)
            Name = name;
            Beers = beers;
        public int Id { get; set; }
        public string Name { get; set; }
        public string[] Beers { get; set; }
        public List<DateOnly> DaysVisited { get; private set; } = new();
    

    Pub 類型包含兩個基本集合:

    Beers 是一系列字串,代表酒吧提供的啤酒品牌。 DaysVisited 是酒吧參觀日期的清單。

    在實際的應用程式中,建立啤酒的實體類型,並擁有啤酒的數據表可能更有意義。 我們在這裡顯示基本集合,以說明其運作方式。 但請記住,只是因為您可以將某個專案模型化為基本集合,並不表示您一定應該使用。

    第二個實體類型代表在英國鄉村散步的狗:

    public class DogWalk
        public DogWalk(string name)
            Name = name;
        public int Id { get; set; }
        public string Name { get; set; }
        public Terrain Terrain { get; set; }
        public List<DateOnly> DaysVisited { get; private set; } = new();
        public Pub ClosestPub { get; set; } = null!;
    public enum Terrain
        Forest,
        River,
        Hills,
        Village,
        Park,
        Beach,
    

    和 一樣 PubDogWalk 也包含參觀日期的集合,以及最接近酒吧的鏈接,因為,你知道,有時狗需要一個飛碟啤酒后長時間行走。

    使用此模型,我們將執行的第一個查詢是一個簡單的 Contains 查詢,以尋找具有數個不同地形之一的所有步行:

    var terrains = new[] { Terrain.River, Terrain.Beach, Terrain.Park };
    var walksWithTerrain = await context.Walks
        .Where(e => terrains.Contains(e.Terrain))
        .Select(e => e.Name)
        .ToListAsync();
    

    這已由目前的 EF Core 版本轉譯,方法是內嵌要尋找的值。 例如,使用 SQL Server 時:

    SELECT [w].[Name]
    FROM [Walks] AS [w]
    WHERE [w].[Terrain] IN (1, 5, 4)
    

    不過,此策略不適用於資料庫查詢快取;如需問題的討論,請參閱 .NET 部落格上的宣佈 EF8 Preview 4

    此處的內嵌值是以沒有 SQL 插入式攻擊機率的方式完成。 使用以下所述的 JSON 變更全都是關於效能,與安全性無關。

    針對EF Core 8,預設值現在是將地形清單當做包含 JSON 集合的單一參數來傳遞。 例如:

    @__terrains_0='[1,5,4]'
    

    查詢接著會在 OpenJson SQL Server 上使用:

    SELECT [w].[Name]
    FROM [Walks] AS [w]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson(@__terrains_0) AS [t]
        WHERE CAST([t].[value] AS int) = [w].[Terrain])
    

    或在 json_each SQLite 上:

    SELECT "w"."Name"
    FROM "Walks" AS "w"
    WHERE EXISTS (
        SELECT 1
        FROM json_each(@__terrains_0) AS "t"
        WHERE "t"."value" = "w"."Terrain")
                  OpenJson 僅適用於 SQL Server 2016 (相容性層級 130) 和更新版本。 您可以藉由將相容性層級設定為 的 UseSqlServer一部分,告訴 SQL Server 您使用舊版。 例如:

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(
                @"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow",
                sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120));
    

    讓我們嘗試不同類型的 Contains 查詢。 在此情況下,我們會在數據行中尋找參數集合的值。 例如,任何股票海因肯的酒吧:

    var beer = "Heineken";
    var pubsWithHeineken = await context.Pubs
        .Where(e => e.Beers.Contains(beer))
        .Select(e => e.Name)
        .ToListAsync();
    

    EF7 新功能的現有檔提供 JSON 對應、查詢和更新的詳細資訊。 本文件現在也適用於 SQLite。

    SELECT [p].[Name]
    FROM [Pubs] AS [p]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson([p].[Beers]) AS [b]
        WHERE [b].[value] = @__beer_0)
                  OpenJson 現在用來從 JSON 數據行擷取值,讓每個值可以比對傳遞的參數。

    我們可以在 參數OpenJson與 數據行上結合 的 用法OpenJson。 例如,若要尋找儲存任何一個各種延隔者的酒吧:

    var beers = new[] { "Carling", "Heineken", "Stella Artois", "Carlsberg" };
    var pubsWithLager = await context.Pubs
        .Where(e => beers.Any(b => e.Beers.Contains(b)))
        .Select(e => e.Name)
        .ToListAsync();
    

    這會轉譯為 SQL Server 上的下列內容:

    SELECT [p].[Name]
    FROM [Pubs] AS [p]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson(@__beers_0) AS [b]
        WHERE EXISTS (
            SELECT 1
            FROM OpenJson([p].[Beers]) AS [b0]
            WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].[value] IS NULL)))
    

    這裡的 @__beers_0 參數值是 ["Carling","Heineken","Stella Artois","Carlsberg"]

    讓我們看看使用包含日期集合之數據行的查詢。 例如,若要尋找今年造訪的酒吧:

    var thisYear = DateTime.Now.Year;
    var pubsVisitedThisYear = await context.Pubs
        .Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
        .Select(e => e.Name)
        .ToListAsync();
    

    這會轉譯為 SQL Server 上的下列內容:

    SELECT [p].[Name]
    FROM [Pubs] AS [p]
    WHERE EXISTS (
        SELECT 1
        FROM OpenJson([p].[DaysVisited]) AS [d]
        WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0)
    

    請注意,因為 EF DATEPART,因此查詢會在這裡使用日期特定函式。 它似乎不像它,但這實際上真的很重要。 因為 EF 知道集合中的內容,所以可以產生適當的 SQL,以搭配參數、函式、其他數據行等使用具類型的值。

    讓我們再次使用日期集合,這次會針對從集合擷取的類型和專案值適當排序。 例如,讓我們以第一次流覽的順序列出酒吧,以及每個酒吧流覽的第一個和最後一個日期:

    var pubsVisitedInOrder = await context.Pubs
        .Select(e => new
            e.Name,
            FirstVisited = e.DaysVisited.OrderBy(v => v).First(),
            LastVisited = e.DaysVisited.OrderByDescending(v => v).First(),
        .OrderBy(p => p.FirstVisited)
        .ToListAsync();
    

    這會轉譯為 SQL Server 上的下列內容:

    SELECT [p].[Name], (
        SELECT TOP(1) CAST([d0].[value] AS date)
        FROM OpenJson([p].[DaysVisited]) AS [d0]
        ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], (
        SELECT TOP(1) CAST([d1].[value] AS date)
        FROM OpenJson([p].[DaysVisited]) AS [d1]
        ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited]
    FROM [Pubs] AS [p]
    ORDER BY (
        SELECT TOP(1) CAST([d].[value] AS date)
        FROM OpenJson([p].[DaysVisited]) AS [d]
        ORDER BY CAST([d].[value] AS date))
    

    最後,我們最後在帶狗散步時,最後參觀最接近的酒吧的頻率為何? 讓我們來看看:

    var walksWithADrink = await context.Walks.Select(
        w => new
            WalkName = w.Name,
            PubName = w.ClosestPub.Name,
            Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
            TotalCount = w.DaysVisited.Count
        }).ToListAsync();
    

    這會轉譯為 SQL Server 上的下列內容:

    SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], (
        SELECT COUNT(*)
        FROM OpenJson([w].[DaysVisited]) AS [d]
        WHERE EXISTS (
            SELECT 1
            FROM OpenJson([p].[DaysVisited]) AS [d0]
            WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
        SELECT COUNT(*)
        FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount]
    FROM [Walks] AS [w]
    INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
    

    並顯示下列資料:

    The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene" walks.
    The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands" walks.
    The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks.
    The White Swan was visited 7 times in 9 "Woodnewton" walks.
    The Eltisley was visited 6 times in 8 "Eltisley" walks.
    Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks.
    Farr Bay Inn was visited 7 times in 9 "Newlands" walks.
    

    看起來啤酒和狗走是一個成功的組合!

    JSON 檔中的基本集合

    在上述所有範例中,基本集合的數據行包含 JSON。 不過,這與將擁有的實體類型對應 至包含EF7 中引進 JSON 文件的數據行不同。 但是,如果該 JSON 檔本身包含基本集合,該怎麼辦? 嗯,上述所有查詢仍然以相同方式運作! 例如,假設我們會將數據

    public class Pub
        public Pub(string name)
            Name = name;
        public int Id { get; set; }
        public string Name { get; set; }
        public BeerData Beers { get; set; } = null!;
        public Visits Visits { get; set; } = null!;
    public class Visits
        public string? LocationTag { get; set; }
        public List<DateOnly> DaysVisited { get; set; } = null!;
    

    此處顯示的程式代碼來自 PrimitiveCollectionsInJsonSample.cs

    我們現在可以執行最後一個查詢的變化,這次,從 JSON 檔擷取數據,包括查詢到檔中包含的基本集合:

    var walksWithADrink = await context.Walks.Select(
        w => new
            WalkName = w.Name,
            PubName = w.ClosestPub.Name,
            WalkLocationTag = w.Visits.LocationTag,
            PubLocationTag = w.ClosestPub.Visits.LocationTag,
            Count = w.Visits.DaysVisited.Count(v => w.ClosestPub.Visits.DaysVisited.Contains(v)),
            TotalCount = w.Visits.DaysVisited.Count
        }).ToListAsync();
    

    這會轉譯為 SQL Server 上的下列內容:

    SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits], '$.LocationTag') AS [PubLocationTag], (
        SELECT COUNT(*)
        FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d]
        WHERE EXISTS (
            SELECT 1
            FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0]
            WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
        SELECT COUNT(*)
        FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS [TotalCount]
    FROM [Walks] AS [w]
    INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
    

    使用 SQLite 時,與類似的查詢:

    SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->> 'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS "PubLocationTag", (
        SELECT COUNT(*)
        FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d"
        WHERE EXISTS (
            SELECT 1
            FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0"
            WHERE "d0"."value" = "d"."value")) AS "Count", json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount"
    FROM "Walks" AS "w"
    INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
    

    請注意,在 SQLite EF Core 上,現在會使用 ->> 運算符,進而產生更容易閱讀且效能較快的查詢。

    將基本集合對應至數據表

    我們上面提到,基本集合的另一個選項是將它們對應至不同的數據表。 問題 #25163 會追蹤此專案的一等支援;如果您對此問題很重要,請務必投票處理此問題。 在實作之前,最好的方法是為基本類型建立包裝類型。 例如,讓我們建立的 型別 Beer

    [Owned]
    public class Beer
        public Beer(string name)
            Name = name;
        public string Name { get; private set; }
    

    請注意,此類型只會包裝基本值-- 它沒有主鍵或任何已定義的外鍵。 這個類型接著可以在 類別中使用 Pub

    public class Pub
        public Pub(string name)
            Name = name;
        public int Id { get; set; }
        public string Name { get; set; }
        public List<Beer> Beers { get; set; } = new();
        public List<DateOnly> DaysVisited { get; private set; } = new();
    

    EF 現在會建立 Beer 數據表,將主鍵和外鍵數據行合成回 Pubs 數據表。 舉例來說,在 SQL Server:

    CREATE TABLE [Beer] (
        [PubId] int NOT NULL,
        [Id] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NOT NULL,
        CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]),
        CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs] ([Id]) ON DELETE CASCADE
    

    JSON 數據行對應的增強功能

    EF8 包含 EF7 中引進的 JSON 數據行對應支援的改善。

    此處顯示的程式代碼來自 JsonColumnsSample.cs

    將元素存取轉譯為 JSON 陣列

    EF8 在執行查詢時,支援在 JSON 陣列中編製索引。 例如,下列查詢會檢查前兩個更新是否在指定的日期之前進行。

    var cutoff = DateOnly.FromDateTime(DateTime.UtcNow - TimeSpan.FromDays(365));
    var updatedPosts = await context.Posts
        .Where(
            p => p.Metadata!.Updates[0].UpdatedOn < cutoff
                 && p.Metadata!.Updates[1].UpdatedOn < cutoff)
        .ToListAsync();
    

    這會在使用 SQL Server 時轉譯成下列 SQL:

    SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
    FROM [Posts] AS [p]
    WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) < @__cutoff_0
      AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) < @__cutoff_0
    

    即使指定的文章沒有任何更新,或只有單一更新,此查詢仍會成功。 在這種情況下, JSON_VALUE 傳回 NULL 和述詞不相符。

    將索引編製成 JSON 陣列也可以用來將陣列中的專案投影到最終結果。 例如,下列查詢會針對每個文章的第一次和第二次更新, UpdatedOn 將日期排除在外。

    var postsAndRecentUpdatesNullable = await context.Posts
        .Select(p => new
            p.Title,
            LatestUpdate = (DateOnly?)p.Metadata!.Updates[0].UpdatedOn,
            SecondLatestUpdate = (DateOnly?)p.Metadata.Updates[1].UpdatedOn
        .ToListAsync();
    

    這會在使用 SQL Server 時轉譯成下列 SQL:

    SELECT [p].[Title],
           CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
           CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
    FROM [Posts] AS [p]
    

    如上所述,如果陣列的 元素不存在, JSON_VALUE 則會傳回 null。 這會在查詢中處理,方法是將投影值轉換成可為 Null 的 DateOnly。 轉換值的替代方法是篩選查詢結果, JSON_VALUE 讓永遠不會傳回 Null。 例如:

    var postsAndRecentUpdates = await context.Posts
        .Where(p => p.Metadata!.Updates[0].UpdatedOn != null
                    && p.Metadata!.Updates[1].UpdatedOn != null)
        .Select(p => new
            p.Title,
            LatestUpdate = p.Metadata!.Updates[0].UpdatedOn,
            SecondLatestUpdate = p.Metadata.Updates[1].UpdatedOn
        .ToListAsync();
    

    這會在使用 SQL Server 時轉譯成下列 SQL:

    SELECT [p].[Title],
           CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate],
           CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate]
    FROM [Posts] AS [p]
          WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) IS NOT NULL)
            AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) IS NOT NULL)
    

    將查詢轉譯成內嵌集合

    EF8 支援針對 JSON 檔中內嵌的基本類型或非基本類型的集合進行查詢。 例如,下列查詢會傳回具有任何任意搜尋字詞清單的所有文章:

    var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search #8", "Search #13", "Search #21", "Search #34" };
    var postsWithSearchTerms = await context.Posts
        .Where(post => post.Metadata!.TopSearches.Any(s => searchTerms.Contains(s.Term)))
        .ToListAsync();
    

    這會在使用 SQL Server 時轉譯成下列 SQL:

    SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
    FROM [Posts] AS [p]
    WHERE EXISTS (
        SELECT 1
        FROM OPENJSON([p].[Metadata], '$.TopSearches') WITH (
            [Count] int '$.Count',
            [Term] nvarchar(max) '$.Term'
        ) AS [t]
        WHERE EXISTS (
            SELECT 1
            FROM OPENJSON(@__searchTerms_0) WITH ([value] nvarchar(max) '$') AS [s]
            WHERE [s].[value] = [t].[Term]))
    

    SQLite 的 JSON 資料行

    EF7 引進了在使用 Azure SQL/SQL Server 時對應至 JSON 數據行的支援。 EF8 會將這項支援延伸至 SQLite 資料庫。 至於 SQL Server 支援,這包括:

  • 從 .NET 類型建置的匯總對應至儲存在 SQLite 數據行中的 JSON 檔
  • 查詢 JSON 資料行,例如篩選和排序檔元素
  • 查詢將 JSON 檔中的項目專案投射到結果中
  • 更新和儲存 JSON 檔的變更
  • EF7 新功能的現有檔提供 JSON 對應、查詢和更新的詳細資訊。 本文件現在也適用於 SQLite。

    EF7 檔中顯示的程式代碼已更新為同時在 SQLite 上執行,可以在 JsonColumnsSample.cs中找到。

    查詢 JSON 數據行

    在 SQLite 上查詢 JSON 數據行時,會使用 函 json_extract 式。 例如,上述檔中的「Chigley 中的作者」查詢:

    var authorsInChigley = await context.Authors
        .Where(author => author.Contact.Address.City == "Chigley")
        .ToListAsync();
    

    使用 SQLite 時會轉譯為下列 SQL:

    SELECT "a"."Id", "a"."Name", "a"."Contact"
    FROM "Authors" AS "a"
    WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley'
    

    更新 JSON 數據行

    針對更新,EF 會在 SQLite 上使用 函 json_set 式。 例如,更新檔中的單一屬性時:

    var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
    arthur.Contact.Address.Country = "United Kingdom";
    await context.SaveChangesAsync();
    

    EF 會產生下列參數:

    info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
          Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']
    

    在 SQLite 上使用 函 json_set 式:

    UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]'))
    WHERE "Id" = @p1
    RETURNING 1;
    

    .NET 和 EF Core 中的 HierarchyId

    Azure SQL 和 SQL Server 具有稱為 hierarchyid 的特殊資料類型,可用來儲存階層式資料。 在此例中,「階層式資料」本質上表示構成樹狀結構的資料,其中每個項目都可能有父系和/或子系。 這類資料包括以下範例:

  • 專案中的一組工作
  • 語言詞彙的分類表
  • 網頁之間的連結圖形
  • 資料庫因此可以使用階層式結構,針對此資料執行查詢。 舉例來說,查詢可以尋找特定項目的上階和相依項目,或尋找階層中特定深度的所有項目。

    .NET 和 EF Core 中的支援

    SQL Server hierarchyid 類型的正式支援最近才來到新式 .NET 平臺(也就是 “.NET Core” )。 此支援的形式 為 Microsoft.SqlServer.Types NuGet 套件,其引進低階 SQL Server 特定類型。 在這裡情況下,低階類型稱為 SqlHierarchyId

    下個層級推出了新的 Microsoft.EntityFrameworkCore.SqlServer.Abstractions 套件,其中包含用於實體類型的更高階 HierarchyId 類型。

    HierarchyId 類型比 SqlHierarchyId 更符合 .NET 的規範,它會在 .NET Framework 類型裝載於 SQL Server 資料庫引擎後進行建模。 HierarchyId 是設計來與 EF Core 搭配使用的,但也可在 EF Core 外部的其他應用程式使用。 Microsoft.EntityFrameworkCore.SqlServer.Abstractions 套件不會參照任何其他套件,因此對已部署的應用程式大小和相依性的影響極小。

    對查詢和更新等 EF Core 功能使用 HierarchyId,需要 Microsoft.EntityFrameworkCore.SqlServer.HierarchyId 套件。 此套件包含 Microsoft.EntityFrameworkCore.SqlServer.AbstractionsMicrosoft.SqlServer.Types 這些可轉移的相依性,因此通常是唯一必需的套件。 安裝套件之後,您可以在應用程式呼叫 HierarchyId 的過程中呼叫 UseHierarchyId,藉此使用 UseSqlServer。 例如:

    options.UseSqlServer(
        connectionString,
        x => x.UseHierarchyId());
    

    EF Core 中的非官方支援hierarchyid已透過 EntityFrameworkCore.SqlServer.HierarchyId 套件提供多年。 此套件已維護為社群與 EF 小組之間的共同作業。 現在,.NET 中有官方支援 hierarchyid ,此社群套件窗體中的程序代碼具有原始參與者的許可權,這是這裡所述的官方套件基礎。 許多感謝多年來涉及的所有人員,包括@aljones、@cutig3r、@huan086、@kmataru@mehdihaghshenas@vyrotek

    HierarchyId 類型可用於實體類型的屬性。 舉例來說,假設我們想要為虛構的半身人的父系族譜建模。 在 Halfling 的實體類型中,HierarchyId 屬性可用於找出族譜中的每個半身人。

    public class Halfling
        public Halfling(HierarchyId pathFromPatriarch, string name, int? yearOfBirth = null)
            PathFromPatriarch = pathFromPatriarch;
            Name = name;
            YearOfBirth = yearOfBirth;
        public int Id { get; private set; }
        public HierarchyId PathFromPatriarch { get; set; }
        public string Name { get; set; }
        public int? YearOfBirth { get; set; }
    

    此處所示和下列範例的程式碼來自 HierarchyIdSample.cs

    如有需要,HierarchyId 可當作索引鍵屬性類型來使用。

    在此例中,族譜是以家族的元老為根。 每個半身人都可以使用 PathFromPatriarch 屬性,從族譜中的元老往下追蹤。 SQL Server 會針對這些路徑使用精簡的二進位格式,但在使用程式碼時,通常會來回剖析人類可讀取的字串表示方式。 在這種表示方式中,每個層級的位置會以 / 字元分隔。 以下圖的族譜為例:

  • Balbo 位於族譜的根部,以 /表示。
  • Balbo 有五個孩子,以 /1//2//3//4//5/ 表示。
  • Balbo 的長子 Mungo 也有五個孩子,以 /1/1//1/2//1/3//1/4//1/5/ 表示。 請注意, HierarchyId 巴爾博 (/1/) 的 是他所有孩子的前置詞。
  • 同樣地,Balbo 的第三子 Ponto 有兩個孩子,以 /3/1//3/2/ 表示。 Ponto 的孩子每個都會前置 HierarchyId,表示為 /3/
  • 族譜再往下都是如此...
  • 下列程式碼會將此族譜插入使用 EF Core 的資料庫:

    await AddRangeAsync(
        new Halfling(HierarchyId.Parse("/"), "Balbo", 1167),
        new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207),
        new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212),
        new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216),
        new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220),
        new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222),
        new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246),
        new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256),
        new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260),
        new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262),
        new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264),
        new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256),
        new Halfling(HierarchyId.Parse("/3/2/"), "Polo"),
        new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264),
        new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290),
        new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310),
        new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303),
        new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302),
        new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306),
        new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302),
        new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308),
        new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311),
        new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310),
        new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344),
        new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346),
        new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348),
        new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350),
        new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368),
        new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350),
        new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381));
    await SaveChangesAsync();
    

    如有需要,十進位值可用來在兩個現有節點之間建立新節點。 例如,/3/2.5/2/ 會位在 /3/2/2//3/3/2/ 之間。

    HierarchyId 會公開多個可用於 LINQ 查詢的方法。

    GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot) 取得代表新節點位置的值,這個節點來自 newRoot 的路徑等於來自 oldRoot 的路徑,能有效將節點移至新位置。 IsDescendantOf(HierarchyId? parent) 取得的值指出此節點是否為 parent 的下階。

    此外,運算子 ==!=<<=>>= 也可使用。

    以下是在 LINQ 查詢使用這些方法的範例。

    在樹狀結構的指定層級取得實體

    下列查詢會使用 GetLevel 傳回族譜中指定層級的所有半身人:

    var generation = await context.Halflings.Where(halfling => halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();
    

    它會轉譯為下列 SQL:

    SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
    FROM [Halflings] AS [h]
    WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0
    

    以迴圈執行此操作,我們就能得到每個世代的半身人:

    Generation 0: Balbo
    Generation 1: Mungo, Pansy, Ponto, Largo, Lily
    Generation 2: Bungo, Belba, Longo, Linda, Bingo, Rosa, Polo, Fosco
    Generation 3: Bilbo, Otho, Falco, Posco, Prisca, Dora, Drogo, Dudo
    Generation 4: Lotho, Poppy, Ponto, Porto, Peony, Frodo, Daisy
    Generation 5: Angelica
                  取得實體的直屬上階

    下列查詢會在取得半身人的名字時,使用 GetAncestor 來尋找半身人的直系祖先:

    async Task<Halfling?> FindDirectAncestor(string name)
        => await context.Halflings
            .SingleOrDefaultAsync(
                ancestor => ancestor.PathFromPatriarch == context.Halflings
                    .Single(descendent => descendent.Name == name).PathFromPatriarch
                    .GetAncestor(1));
    

    它會轉譯為下列 SQL:

    SELECT TOP(2) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
    FROM [Halflings] AS [h]
    WHERE [h].[PathFromPatriarch] = (
        SELECT TOP(1) [h0].[PathFromPatriarch]
        FROM [Halflings] AS [h0]
        WHERE [h0].[Name] = @__name_0).GetAncestor(1)
    

    對半身人「Bilbo」執行此查詢會傳回「Bungo」。

    取得實體的直接子代

    下列查詢也會使用 GetAncestor,但這次會尋找半元的直接子代,因為有半分號的名稱:

    IQueryable<Halfling> FindDirectDescendents(string name)
        => context.Halflings.Where(
            descendent => descendent.PathFromPatriarch.GetAncestor(1) == context.Halflings
                .Single(ancestor => ancestor.Name == name).PathFromPatriarch);
    

    它會轉譯為下列 SQL:

    SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
    FROM [Halflings] AS [h]
    WHERE [h].[PathFromPatriarch].GetAncestor(1) = (
        SELECT TOP(1) [h0].[PathFromPatriarch]
        FROM [Halflings] AS [h0]
        WHERE [h0].[Name] = @__name_0)
    

    對半身人「Mungo」執行此查詢會傳回「Bungo」、「Belba」、「Longo」和「Linda」。

    取得實體的所有上階

    往上或往下搜尋單一層級或指定層級數量時,GetAncestor 很實用。 另一方面,IsDescendantOf 對於尋找所有上階或相依項目很有用。 舉例來說,下列查詢在取得半身人的名字後,會使用 IsDescendantOf 來尋找半身人的所有祖先:

    IQueryable<Halfling> FindAllAncestors(string name)
        => context.Halflings.Where(
                ancestor => context.Halflings
                    .Single(
                        descendent =>
                            descendent.Name == name
                            && ancestor.Id != descendent.Id)
                    .PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
            .OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel());
                  IsDescendantOf 會對本身傳回 true,因此需要在上述查詢中篩除。

    它會轉譯為下列 SQL:

    SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
    FROM [Halflings] AS [h]
    WHERE (
        SELECT TOP(1) [h0].[PathFromPatriarch]
        FROM [Halflings] AS [h0]
        WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id]).IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
    ORDER BY [h].[PathFromPatriarch].GetLevel() DESC
    

    半身人「Bilbo」執行此查詢會傳回「Bungo」、「Mungo」和「Balbo」。

    取得實體的所有子代

    下列查詢也會使用 ,但這次會使用 IsDescendantOf半元的所有子代,因為有半個子系的名稱:

    IQueryable<Halfling> FindAllDescendents(string name)
        => context.Halflings.Where(
                descendent => descendent.PathFromPatriarch.IsDescendantOf(
                    context.Halflings
                        .Single(
                            ancestor =>
                                ancestor.Name == name
                                && descendent.Id != ancestor.Id)
                        .PathFromPatriarch))
            .OrderBy(descendent => descendent.PathFromPatriarch.GetLevel());
    

    它會轉譯為下列 SQL:

    SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
    FROM [Halflings] AS [h]
    WHERE [h].[PathFromPatriarch].IsDescendantOf((
        SELECT TOP(1) [h0].[PathFromPatriarch]
        FROM [Halflings] AS [h0]
        WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id])) = CAST(1 AS bit)
    ORDER BY [h].[PathFromPatriarch].GetLevel()
    

    對半身人「Mungo」執行此查詢,會傳回「Bungo」、「Belba」、「Longo」、「Linda」、「Bingo」、「Bilbo」、「Otho」、「Falco」、「Lotho」和「Poppy」。

    尋找共有的上階

    關於這個特定族譜的最常見問題之一是,「誰是 Frodo 和 Bilbo 的共同祖先?」我們可用 IsDescendantOf 來撰寫上述查詢:

    async Task<Halfling?> FindCommonAncestor(Halfling first, Halfling second)
        => await context.Halflings
            .Where(
                ancestor => first.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch)
                            && second.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
            .OrderByDescending(ancestor => ancestor.PathFromPatriarch.GetLevel())
            .FirstOrDefaultAsync();
    

    它會轉譯為下列 SQL:

    SELECT TOP(1) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]
    FROM [Halflings] AS [h]
    WHERE @__first_PathFromPatriarch_0.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
      AND @__second_PathFromPatriarch_1.IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
    ORDER BY [h].[PathFromPatriarch].GetLevel() DESC
    

    以「Bilbo」和「Frodo」來執行此查詢,我們得知他們的共同祖先是「Balbo」。

    一般的變更追蹤SaveChanges 機制可用來更新 hierarchyid 欄。

    重新建立子階層的父系

    舉例來說,我想大家都記得 SR 1752 (也就是「LongoGate」) 事件:DNA 測試顯示 Longo 其實不是 Mungo 的兒子,而是 Ponto 的兒子! 這起事件的一個後果是族譜需要重寫。 特別是,朗戈和他的所有後裔都需要從蒙戈重新養育到龐托。 此動作可使用 GetReparentedValue。 例如,會查詢第一個 「Longo」 及其所有子代:

    var longoAndDescendents = await context.Halflings.Where(
            descendent => descendent.PathFromPatriarch.IsDescendantOf(
                context.Halflings.Single(ancestor => ancestor.Name == "Longo").PathFromPatriarch))
        .ToListAsync();
                  GetReparentedValue 會用來更新 Longo 和每個後裔的 HierarchyId,接著呼叫 SaveChangesAsync

    foreach (var descendent in longoAndDescendents)
        descendent.PathFromPatriarch
            = descendent.PathFromPatriarch.GetReparentedValue(
                mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
    await context.SaveChangesAsync();
    

    這會產生下列資料庫更新:

    SET NOCOUNT ON;
    UPDATE [Halflings] SET [PathFromPatriarch] = @p0
    OUTPUT 1
    WHERE [Id] = @p1;
    UPDATE [Halflings] SET [PathFromPatriarch] = @p2
    OUTPUT 1
    WHERE [Id] = @p3;
    UPDATE [Halflings] SET [PathFromPatriarch] = @p4
    OUTPUT 1
    WHERE [Id] = @p5;
    

    使用這些參數:

     @p1='9',
     @p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object),
     @p3='16',
     @p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object),
     @p5='23',
     @p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object)
                  HierarchyId 屬性的參數值會以精簡的二進位格式傳送至資料庫。

    更新之後,查詢 「Mungo」 的子系會傳回 「Bungo」 “ Belba”、“Linda”、“Bingo”、“Bilbo”、“Falco”和 “Popy”,同時查詢 “Ponto” 的子系會傳回 “Longo”、“Rosa”、“Polo”、“Otho”、“Posco”、“Prisca”、“Lotho”、“Ponto”、“波爾圖”、“牡丹”和 “Angelica”。

    未對應的原始 SQL 查詢

    EF7 引進傳 回純量類型的原始 SQL 查詢。 這在 EF8 中增強,可包含傳回任何可對應 CLR 類型的未經處理的 SQL 查詢,而不會在 EF 模型中包含該類型。

    此處顯示的程式代碼來自 RawSqlSample.cs

    使用未對應的型別的查詢會使用 SqlQuerySqlQueryRaw來執行。 前者會使用字串插補來參數化查詢,這有助於確保所有非常數值都參數化。 例如,請考慮下列資料庫數據表:

    CREATE TABLE [Posts] (
        [Id] int NOT NULL IDENTITY,
        [Title] nvarchar(max) NOT NULL,
        [Content] nvarchar(max) NOT NULL,
        [PublishedOn] date NOT NULL,
        [BlogId] int NOT NULL,
                  SqlQuery 可用來查詢此數據表,並傳回類型的實例 BlogPost ,其屬性對應至數據表中的數據行:

    public class BlogPost
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }
        public DateOnly PublishedOn { get; set; }
        public int BlogId { get; set; }
    
    var start = new DateOnly(2022, 1, 1);
    var end = new DateOnly(2023, 1, 1);
    var postsIn2022 =
        await context.Database
            .SqlQuery<BlogPost>($"SELECT * FROM Posts as p WHERE p.PublishedOn >= {start} AND p.PublishedOn < {end}")
            .ToListAsync();
    

    此查詢會參數化並執行為:

    SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1
    

    用於查詢結果的類型可以包含EF Core 支援的常見對應建構,例如參數化建構函式和對應屬性。 例如:

    public class BlogPost
        public BlogPost(string blogTitle, string content, DateOnly publishedOn)
            BlogTitle = blogTitle;
            Content = content;
            PublishedOn = publishedOn;
        public int Id { get; private set; }
        [Column("Title")]
        public string BlogTitle { get; set; }
        public string Content { get; set; }
        public DateOnly PublishedOn { get; set; }
        public int BlogId { get; set; }
    

    以這種方式使用的型別沒有定義索引鍵,而且不能與其他型別有關聯性。 具有關聯性的型別必須在模型中對應。

    使用的類型必須具有結果集中每個值的屬性,但不需要比對資料庫中的任何數據表。 例如,下列類型只代表每個文章的資訊子集,並包含來自數據表的 Blogs 部落格名稱:

    public class PostSummary
        public string BlogName { get; set; } = null!;
        public string PostTitle { get; set; } = null!;
        public DateOnly? PublishedOn { get; set; }
    

    而且可以使用與之前相同的方式來查詢 SqlQuery

    var cutoffDate = new DateOnly(2022, 1, 1); var summaries = await context.Database.SqlQuery<PostSummary>( @$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn FROM Posts AS p INNER JOIN Blogs AS b ON p.BlogId = b.Id WHERE p.PublishedOn >= {cutoffDate}") .ToListAsync();

    的其中一個不錯功能 SqlQuery 是,它會傳回 IQueryable 可使用 LINQ 撰寫的 。 例如,可以將 'Where' 子句新增至上述查詢:

    var summariesIn2022 =
        await context.Database.SqlQuery<PostSummary>(
                @$"SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
                   FROM Posts AS p
                   INNER JOIN Blogs AS b ON p.BlogId = b.Id")
            .Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
            .ToListAsync();
    

    這會以下列方式執行:

    SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn]
    FROM (
             SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn
             FROM Posts AS p
                      INNER JOIN Blogs AS b ON p.BlogId = b.Id
         ) AS [n]
    WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2
    

    此時值得記住,上述所有專案都可以完全在LINQ中完成,而不需要撰寫任何SQL。 這包括傳回未對應的型別實例,例如 PostSummary。 例如,上述查詢可以用 LINQ 撰寫為:

    var summaries =
        await context.Posts.Select(
                p => new PostSummary
                    BlogName = p.Blog.Name,
                    PostTitle = p.Title,
                    PublishedOn = p.PublishedOn,
            .Where(p => p.PublishedOn >= start && p.PublishedOn < end)
            .ToListAsync();
    

    這會轉譯成更簡潔的 SQL:

    SELECT [b].[Name] AS [BlogName], [p].[Title] AS [PostTitle], [p].[PublishedOn]
    FROM [Posts] AS [p]
    INNER JOIN [Blogs] AS [b] ON [p].[BlogId] = [b].[Id]
    WHERE [p].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1
    

    EF 能夠在負責整個查詢時產生更簡潔的 SQL,而不是透過使用者提供的 SQL 撰寫時,因為就先前的案例而言,查詢的完整語意可供 EF 使用。

    到目前為止,所有查詢都已直接針對數據表執行。 SqlQuery 也可以用來傳回檢視的結果,而不需要對應 EF 模型中的檢視類型。 例如:

    var summariesFromView =
        await context.Database.SqlQuery<PostSummary>(
                @$"SELECT * FROM PostAndBlogSummariesView")
            .Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
            .ToListAsync();
    

    同樣地, SqlQuery 可用於函式的結果:

    var summariesFromFunc =
        await context.Database.SqlQuery<PostSummary>(
                @$"SELECT * FROM GetPostsPublishedAfter({cutoffDate})")
            .Where(p => p.PublishedOn < end)
            .ToListAsync();
    

    當傳回 IQueryable 的 是檢視或函式的結果時,可以撰寫,就如同數據表查詢的結果一樣。 預存程式也可以使用 來執行 SqlQuery,但大部分的資料庫都不支援透過它們撰寫。 例如:

    var summariesFromStoredProc =
        await context.Database.SqlQuery<PostSummary>(
                @$"exec GetRecentPostSummariesProc")
            .ToListAsync();
    

    延遲載入的增強功能

    無追蹤查詢的延遲載入

    EF8 新增對的支援。 這表示不追蹤查詢可以接著在無追蹤查詢所傳回的實體上延遲載入導覽。

    下列延遲載入範例的程式代碼來自 LazyLoadingSample.cs

    例如,請考慮不追蹤部落格的查詢:

    var blogs = await context.Blogs.AsNoTracking().ToListAsync();
    

    如果 Blog.Posts 已設定延遲載入,例如,使用延遲載入 Proxy,則存取 Posts 會導致它從資料庫載入:

    Console.WriteLine();
    Console.Write("Choose a blog: ");
    if (int.TryParse(ReadLine(), out var blogId))
        Console.WriteLine("Posts:");
        foreach (var post in blogs[blogId - 1].Posts)
            Console.WriteLine($"  {post.Title}");
    

    EF8 也會報告是否針對內容未追蹤的實體載入指定的導覽。 例如:

    foreach (var blog in blogs)
        if (context.Entry(blog).Collection(e => e.Posts).IsLoaded)
            Console.WriteLine($" Posts for blog '{blog.Name}' are loaded.");
    

    以這種方式使用延遲載入時,有幾個重要考慮:

  • 延遲載入只會成功,直到 DbContext 用來查詢實體的 處置為止。
  • 以這種方式查詢的實體會維護其 DbContext參考,即使它們並未受到追蹤也一樣。 如果實體實例的存留期很長,就應該小心避免記憶體流失。
  • 藉由設定實體的狀態來 EntityState.Detached 明確中斷鏈接實體,而延遲載入的參考 DbContext 將無法再運作。
  • 請記住,所有延遲載入都會使用同步 I/O,因為無法以異步方式存取屬性。
  • 從未追蹤的實體延遲載入適用於 延遲載入 Proxy沒有 Proxy 的延遲載入。

    從未追蹤的實體明確載入

    EF8 支援在未追蹤的實體上載入導覽,即使未設定延遲載入的實體或流覽也一併載入。 不同於延遲載入, 此明確載入 可以異步完成。 例如:

    await context.Entry(blog).Collection(e => e.Posts).LoadAsync();
    

    退出退出特定導覽的延遲載入

    EF8 允許設定特定導覽,使其不延遲載入,即使其他所有專案都設定為這麼做也一樣。 例如,若要將 Post.Author 瀏覽設定為不延遲載入,請執行下列動作:

    modelBuilder
        .Entity<Post>()
        .Navigation(p => p.Author)
        .EnableLazyLoading(false);
    

    停用延遲載入,這適用於 延遲載入 Proxy沒有 Proxy 的延遲載入。

    延遲載入 Proxy 的運作方式是覆寫虛擬導覽屬性。 在傳統EF6 應用程式中,常見的 Bug 來源會忘記讓流覽虛擬,因為流覽會以無訊息方式不延遲載入。 因此,當導覽不是虛擬時,EF Core Proxy 預設會擲回。

    這可以在EF8 中變更為選擇加入傳統 EF6 行為,如此一來,只要讓流覽非虛擬,即可將瀏覽設為不延遲載入。 這個選擇加入設定為呼叫 UseLazyLoadingProxies的一部分。 例如:

    optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());
    

    存取追蹤的實體

    依主要、替代或外鍵查閱追蹤的實體

    在內部,EF 會維護數據結構,以便依主要、替代或外鍵尋找追蹤的實體。 當追蹤新實體或關聯性變更時,這些數據結構可用來有效修正相關實體。

    EF8 包含新的公用 API,讓應用程式現在可以使用這些數據結構有效率地查閱追蹤的實體。 這些 API 是透過 LocalView<TEntity> 實體類型的 存取。 例如,若要依其主鍵查閱追蹤實體:

    var blogEntry = context.Blogs.Local.FindEntry(2)!;
    

    此處顯示的程式代碼來自 LookupByKeySample.cs

    方法 FindEntry 會傳 EntityEntry<TEntity> 回所追蹤實體的 ,如果 null 未追蹤具有指定索引鍵的實體,則傳回 。 和上 LocalView的所有方法一樣,即使找不到實體,也不會查詢資料庫。 傳回的專案包含實體本身,以及追蹤資訊。 例如:

    Console.WriteLine($"Blog '{blogEntry.Entity.Name}' with key {blogEntry.Entity.Id} is tracked in the '{blogEntry.State}' state.");
    

    除了主鍵以外,查詢實體時,需要指定屬性名稱。 例如,若要依替代索引鍵查閱:

    var siteEntry = context.Websites.Local.FindEntry(nameof(Website.Uri), new Uri("https://www.bricelam.net/"))!;
    

    或者,若要透過唯一的外鍵查閱:

    var blogAtSiteEntry = context.Blogs.Local.FindEntry(nameof(Blog.SiteUri), new Uri("https://www.bricelam.net/"))!;
    

    到目前為止,查閱一律會傳回單一專案或 null。 不過,某些查閱可能會傳回一個以上的專案,例如透過非唯一外鍵查閱時。 方法 GetEntries 應該用於這些查閱。 例如:

    var postEntries = context.Posts.Local.GetEntries(nameof(Post.BlogId), 2);
    

    在這些情況下,用於查閱的值是主鍵、替代索引鍵或外鍵值。 EF 會針對這些查閱使用其內部數據結構。 不過,依值查閱也可用於任何屬性的值或屬性的組合。 例如,若要尋找所有封存文章:

    var archivedPostEntries = context.Posts.Local.GetEntries(nameof(Post.Archived), true);
    

    此查閱需要掃描所有追蹤 Post 的實例,因此會比索引鍵查閱效率低。 不過,使用的查詢通常仍然比天真查詢 ChangeTracker.Entries<TEntity>()更快。

    最後,您也可以對複合索引鍵、多個屬性的其他組合,或在編譯時期不知道屬性類型時執行查閱。 例如:

    var postTagEntry = context.Set<PostTag>().Local.FindEntryUntyped(new object[] { 4, "TagEF" });
    

    歧視性數據行的長度上限

    在 EF8 中,用於 TPH 繼承對應的 字串歧視性數據行現在已設定長度上限。 此長度會計算為涵蓋所有已定義之歧視性值的最小 Fibonacci 數位。 例如,請考慮下列階層:

    public abstract class Document
        public int Id { get; set; }
        public string Title { get; set; }
    public abstract class Book : Document
        public string? Isbn { get; set; }
    public class PaperbackEdition : Book
    public class HardbackEdition : Book
    public class Magazine : Document
        public int IssueNumber { get; set; }
    

    使用歧視性值的類別名稱慣例,這裡的可能值為 “PaperbackEdition”、“HardbackEdition” 和 “Magazine”,因此將歧視性數據行設定為長度上限為 21。 例如,使用 SQL Server 時:

    CREATE TABLE [Documents] (
        [Id] int NOT NULL IDENTITY,
        [Title] nvarchar(max) NOT NULL,
        [Discriminator] nvarchar(21) NOT NULL,
        [Isbn] nvarchar(max) NULL,
        [IssueNumber] int NULL,
        CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]),
    

    Fibonacci 數位可用來限制移轉產生的次數,以在將新類型新增至階層時變更數據行長度。

    SQL Server 上支援的 DateOnly/TimeOnly

    DateOnlyTimeOnly 類型是在 .NET 6 中引進的,自推出以來,已支援數個資料庫提供者(例如 SQLite、MySQL 和 PostgreSQL)。 針對 SQL Server,以 .NET 6 為目標的 Microsoft.Data.SqlClient 套件最新版本允許 ErikEJ 在 ADO.NET 層級新增這些類型的支援。 這反過來又為 EF8 的支援 DateOnly 鋪平了實體類型中的 屬性 TimeOnly

    DateOnlyTimeOnly 可以使用來自 @ErikEJErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly 社群套件,在 EF Core 6 和 7 中使用。

    例如,請考慮下列英國學校的 EF 模型:

    public class School
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public DateOnly Founded { get; set; }
        public List<Term> Terms { get; } = new();
        public List<OpeningHours> OpeningHours { get; } = new();
    public class Term
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public DateOnly FirstDay { get; set; }
        public DateOnly LastDay { get; set; }
        public School School { get; set; } = null!;
    [Owned]
    public class OpeningHours
        public OpeningHours(DayOfWeek dayOfWeek, TimeOnly? opensAt, TimeOnly? closesAt)
            DayOfWeek = dayOfWeek;
            OpensAt = opensAt;
            ClosesAt = closesAt;
        public DayOfWeek DayOfWeek { get; private set; }
        public TimeOnly? OpensAt { get; set; }
        public TimeOnly? ClosesAt { get; set; }
    

    此處顯示的程式代碼來自 DateOnlyTimeOnlySample.cs

    此模型僅代表英國學校,並儲存當地時間(GMT)時間。 處理不同的時區會使此程序代碼大幅複雜。 請注意,使用 DateTimeOffset 並無説明,因為開啟和關閉時間有不同的位移,視日光節約時間是否作用中而定。

    這些實體類型會在使用 SQL Server 時對應至下表。 請注意, DateOnly 屬性會對應至 date 數據行,而 TimeOnly 屬性會對應至數據行 time

    CREATE TABLE [Schools] (
        [Id] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NOT NULL,
        [Founded] date NOT NULL,
        CONSTRAINT [PK_Schools] PRIMARY KEY ([Id]));
    CREATE TABLE [OpeningHours] (
        [SchoolId] int NOT NULL,
        [Id] int NOT NULL IDENTITY,
        [DayOfWeek] int NOT NULL,
        [OpensAt] time NULL,
        [ClosesAt] time NULL,
        CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]),
        CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);
    CREATE TABLE [Term] (
        [Id] int NOT NULL IDENTITY,
        [Name] nvarchar(max) NOT NULL,
        [FirstDay] date NOT NULL,
        [LastDay] date NOT NULL,
        [SchoolId] int NOT NULL,
        CONSTRAINT [PK_Term] PRIMARY KEY ([Id]),
        CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE);
    

    使用