高级表映射

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

另请注意,如有必要,可以为每个表指定不同的列名称。 若要配置主表的列名,请参阅特定于表的方面配置

配置链接外键

连接映射表的 FK 的目标是声明其所针对的相同属性。 通常不会在数据库中创建,因为它是冗余的。 但是,当实体类型映射到多个表时,会出现异常。 若要更改其方面,可以使用关系配置 Fluent API

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

限制

  • 实体拆分不能用于层次结构中的实体类型。
  • 对于主表中的任何行,每个拆分表中都必须有一行(片段不是可选的)。

特定于表的方面配置

某些映射模式会导致同一 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");
        });