共用方式為


進階資料表對應

EF Core 在將實體類型對應至資料庫中的數據表時,提供許多彈性。 當您需要使用 EF 未建立的資料庫時,這會變得更加有用。

以下技術以資料表的方式描述,但對應到檢視時,也可以達到相同的結果。

檔案分割

EF Core 允許將兩個或多個實體對應至單一數據列。 這稱為 數據表分割數據表共用

設定

若要使用數據表分割實體類型,必須將實體類型對應至相同的數據表,讓主鍵對應至相同的數據行,以及至少在相同數據表中設定一個實體類型之主鍵與另一個實體類型之間的關聯性。

數據表分割的常見案例是只使用數據表中的數據行子集來提升效能或封裝。

在此範例中,Order 代表 DetailedOrder 的子集。

public class Order
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public DetailedOrder DetailedOrder { get; set; }
}
public class DetailedOrder
{
    public int Id { get; set; }
    public OrderStatus? Status { get; set; }
    public string BillingAddress { get; set; }
    public string ShippingAddress { get; set; }
    public byte[] Version { get; set; }
}

除了必要的組態之外,我們呼叫 Property(o => o.Status).HasColumnName("Status") 來對應 DetailedOrder.Status 至與 Order.Status相同的數據行。

modelBuilder.Entity<DetailedOrder>(
    dob =>
    {
        dob.ToTable("Orders");
        dob.Property(o => o.Status).HasColumnName("Status");
    });

modelBuilder.Entity<Order>(
    ob =>
    {
        ob.ToTable("Orders");
        ob.Property(o => o.Status).HasColumnName("Status");
        ob.HasOne(o => o.DetailedOrder).WithOne()
            .HasForeignKey<DetailedOrder>(o => o.Id);
        ob.Navigation(o => o.DetailedOrder).IsRequired();
    });

小提示

如需更多內容, 請參閱完整的範例專案

用法

使用數據表分割來儲存和查詢實體的方式與其他實體相同:

using (var context = new TableSplittingContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(
        new Order
        {
            Status = OrderStatus.Pending,
            DetailedOrder = new DetailedOrder
            {
                Status = OrderStatus.Pending,
                ShippingAddress = "221 B Baker St, London",
                BillingAddress = "11 Wall Street, New York"
            }
        });

    await context.SaveChangesAsync();
}

using (var context = new TableSplittingContext())
{
    var pendingCount = await context.Orders.CountAsync(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"Current number of pending orders: {pendingCount}");
}

using (var context = new TableSplittingContext())
{
    var order = await context.DetailedOrders.FirstAsync(o => o.Status == OrderStatus.Pending);
    Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}

選擇性相依實體

如果相依實體所使用的所有數據行都在 NULL 資料庫中,則查詢時不會生成任何實例。 這可讓選擇性相依實體模型化,其中主體上的關聯性屬性會是 Null。 請注意,如果所有的相依屬性都是可選的,且都設為null,這種情況也可能發生,這可能是預期之外的結果。

不過,其他檢查可能會影響查詢效能。 此外,如果相依實體類型有自己的相依性,則判斷是否應該建立實例變成非簡單。 若要避免這些問題,可以將相依實體類型標示為必須, 如需詳細資訊,請參閱 必須的一對一相依實體

同步令牌

如果共用數據表的任何實體類型都有並行令牌,則它也必須包含在所有其他實體類型中。 當只有對應至相同數據表的其中一個實體更新時,才需要這麼做,以避免過時的並行令牌值。

若要避免將並行令牌暴露給使用程式碼,可以將其建立為陰影屬性

modelBuilder.Entity<Order>()
    .Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

modelBuilder.Entity<DetailedOrder>()
    .Property(o => o.Version).IsRowVersion().HasColumnName("Version");

遺產

建議您先閱讀 繼承 的專用頁面,再繼續進行本節。

使用數據表分割的相依型別可以有繼承階層,但有一些限制:

  • 相依實體類型 無法使用 TPC 對應,因為衍生類型無法對應至相同的數據表。
  • 相依實體類型 可以使用 TPT 對應,但只有根實體類型可以使用數據表分割。
  • 如果主體實體類型使用 TPC,則只有沒有任何子系的實體類型可以使用數據表分割。 否則,相依數據行必須在對應至衍生型別的數據表上複製,使所有互動複雜化。

實體分割

EF Core 允許將實體對應至兩個或多個數據表中的數據列。 這稱為 實體分割

設定

例如,請考慮具有三個保存客戶數據之數據表的資料庫:

  • Customers客戶信息的數據表
  • PhoneNumbers客戶的電話號碼數據表
  • Addresses用於顯示客戶地址的表格

以下是 SQL Server 中這些資料表的定義:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
    
CREATE TABLE [PhoneNumbers] (
    [CustomerId] int NOT NULL,
    [PhoneNumber] nvarchar(max) NULL,
    CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Addresses] (
    [CustomerId] int NOT NULL,
    [Street] nvarchar(max) NOT NULL,
    [City] nvarchar(max) NOT NULL,
    [PostCode] nvarchar(max) NULL,
    [Country] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
    CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);

每個數據表通常會對應到自己的實體類型,且類型之間具有關聯性。 不過,如果這三個數據表一律一起使用,則將其全部對應至單一實體類型會比較方便。 例如:

public class Customer
{
    public Customer(string name, string street, string city, string? postCode, string country)
    {
        Name = name;
        Street = street;
        City = city;
        PostCode = postCode;
        Country = country;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public string? PhoneNumber { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string? PostCode { get; set; }
    public string Country { get; set; }
}

這會在 EF7 中呼叫 SplitToTable 實體類型中的每個分割來達成此目的。 例如,下列程式代碼會將 Customer 實體類型分割為如上所示的 CustomersPhoneNumbersAddresses 資料表:

modelBuilder.Entity<Customer>(
    entityBuilder =>
    {
        entityBuilder
            .ToTable("Customers")
            .SplitToTable(
                "PhoneNumbers",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.PhoneNumber);
                })
            .SplitToTable(
                "Addresses",
                tableBuilder =>
                {
                    tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
                    tableBuilder.Property(customer => customer.Street);
                    tableBuilder.Property(customer => customer.City);
                    tableBuilder.Property(customer => customer.PostCode);
                    tableBuilder.Property(customer => customer.Country);
                });
    });

另請注意,如有必要,可以為每個數據表指定不同的數據行名稱。 若要設定主數據表的數據行名稱,請參閱 數據表特定的 Facet 組態

設定連結外鍵

鏈接對應數據表的 FK 是以宣告其所在的相同屬性為目標。 通常不會在資料庫中建立,因為它會是多餘的。 但是實體類型對應到一個以上的數據表時,會有例外狀況。 若要變更其面向,您可以使用 關聯配置流暢 API

modelBuilder.Entity<Customer>()
    .HasOne<Customer>()
    .WithOne()
    .HasForeignKey<Customer>(a => a.Id)
    .OnDelete(DeleteBehavior.Restrict);

局限性

  • 實體分割不能用於階層中的實體類型。
  • 對於主數據表中的任何數據列,每個分割數據表中都必須有一個數據列(片段不是選擇性的)。

數據表特定的 Facet 組態

某些對應模式可能會導致相同的 CLR 屬性映射到多個不同表格中的某個欄位。 EF7 可讓這些數據行有不同的名稱。 例如,請考慮簡單的繼承階層:

public abstract class Animal
{
    public int Id { get; set; }
    public string Breed { get; set; } = null!;
}

public class Cat : Animal
{
    public string? EducationalLevel { get; set; }
}

public class Dog : Animal
{
    public string? FavoriteToy { get; set; }
}

使用 TPT 繼承對應策略,這些類型會對應至三個數據表。 不過,每個數據表中的主鍵數據行可能有不同的名稱。 例如:

CREATE TABLE [Animals] (
    [Id] int NOT NULL IDENTITY,
    [Breed] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);

CREATE TABLE [Cats] (
    [CatId] int NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
    CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
    CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);

EF7 允許使用巢狀表格建構器來設定此對應。

modelBuilder.Entity<Animal>().ToTable("Animals");

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));

透過 TPC 繼承對應, Breed 屬性也可以對應至不同資料表中的不同數據行名稱。 例如,請考慮下列 TPC 數據表:

CREATE TABLE [Cats] (
    [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [CatBreed] nvarchar(max) NOT NULL,
    [EducationalLevel] nvarchar(max) NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);

CREATE TABLE [Dogs] (
    [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [DogBreed] nvarchar(max) NOT NULL,
    [FavoriteToy] nvarchar(max) NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);

EF7 支援此資料表映射:

modelBuilder.Entity<Animal>().UseTpcMappingStrategy();

modelBuilder.Entity<Cat>()
    .ToTable(
        "Cats",
        builder =>
        {
            builder.Property(cat => cat.Id).HasColumnName("CatId");
            builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
        });

modelBuilder.Entity<Dog>()
    .ToTable(
        "Dogs",
        builder =>
        {
            builder.Property(dog => dog.Id).HasColumnName("DogId");
            builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
        });