Share via


進階資料表對應

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())
{
    context.Database.EnsureDeleted();
    context.Database.EnsureCreated();

    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"
            }
        });

    context.SaveChanges();
}

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

using (var context = new TableSplittingContext())
{
    var order = context.DetailedOrders.First(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 是以宣告其所在的相同屬性為目標。 通常不會在資料庫中建立,因為它會是多餘的。 但是實體類型對應到一個以上的資料表時,會有例外狀況。 若要變更其 Facet,您可以使用 關聯性組態 Fluent 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");
        });