高级表映射

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

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

配置链接外键

链接映射表的 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");
        });