关系中的外键和主键

所有一对一一对多关系均由依赖端上的外键所定义,用于引用主体端上的主键或备用键。 为方便起见,此主键或备用键称为关系的“主键”。 多对多关系由两个一对多关系组成,每个关系本身由引用主键的外键所定义。

提示

可以在 ForeignAndPrincipalKeys.cs 中找到以下代码。

外键

通常按约定发现组成外键的一个或多个属性。 还可以使用映射属性或在模型生成 API 中使用 HasForeignKey 显式配置属性。 HasForeignKey 可与 Lambda 表达式一起使用。 例如,对于由单个属性组成的外键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.ContainingBlogId);
}

或者,对于由多个属性组成的组合外键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => new { e.ContainingBlogId1, e.ContainingBlogId2 });
}

提示

在模型生成 API 中使用 Lambda 表达式可确保属性可用于代码分析和重构,并为 API 提供属性类型,以供在更多链接的方法中使用。

HasForeignKey 还可以将外键属性的名称作为字符串传递。 例如,对于单个属性:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("ContainingBlogId");
}

或者,对于组合外键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("ContainingBlogId1", "ContainingBlogId2");
}

在以下情况下,使用字符串非常有用:

  • 一个或多个属性是私有的。
  • 一个或多个属性不存在于实体类型上,应创建为阴影属性
  • 属性名称是根据模型生成过程的一些输入计算或构造的。

不可为空的外键列

可选关系和必需关系中所述,外键属性的可为空性决定了关系是可选关系还是必需关系。 但是,可为空的外键属性可用于使用 [Required] 特性或通过在模型生成 API 中调用 IsRequired 来建立必需的关系。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .IsRequired();
}

或者,如果外键是按约定发现的,则无需调用 HasForeignKey 即可使用 IsRequired

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .IsRequired();
}

这样做的最终结果是,即使外键属性可为空,数据库中的外键列也不可为空。 也可以根据需要显式配置外键属性本身来实现相同的操作。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property(e => e.BlogId)
        .IsRequired();
}

阴影外键

外键属性可以创建为阴影属性。 EF 模型中存在阴影属性,但 .NET 类型中不存在。 EF 在内部跟踪属性值和状态。

当希望从应用程序代码/业务逻辑使用的域模型中隐藏外键的关系概念时,通常会使用阴影外键。 然后,此应用程序代码完全通过导航操作关系。

提示

如果要序列化实体(例如通过网络发送),则当实体不采用对象/图表形式时,外键值可能是保持关系信息不变的有用方法。 因此,为实现此目的,务实的做法是在 .NET 类型中保留外键属性。 外键属性可以是私有的,这是一个折中的办法,既可以避免公开外键,又允许其值随实体一起传输。

阴影外键属性通常是按约定创建的。 如果 HasForeignKey 的参数与任何 .NET 属性都不匹配,也会创建阴影外键。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("MyBlogId");
}

按照约定,阴影外键从关系中的主体键获取其类型。 除非将关系检测为或配置为必需,否则此类型可为空。

也可以显式创建阴影外键属性,这对于配置属性的 facet 十分有帮助。 例如,若要使属性不可为空:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .Property<string>("MyBlogId")
        .IsRequired();

    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey("MyBlogId");
}

提示

按照约定,外键属性从关系中的主体键中继承 facet,例如最大长度和 Unicode 支持。 因此,很少需要在外键属性上显式配置 facet。

如果给定的名称与实体类型的任何属性都不匹配,则可以使用 ConfigureWarnings 禁用阴影属性的创建。 例如:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Throw(CoreEventId.ShadowPropertyCreated));

外键约束名称

根据约定,外键约束名为 FK_<dependent type name>_<principal type name>_<foreign key property name>。 对于组合外键,<foreign key property name> 将成为外键属性名称的下划线分隔列表。

可以使用 HasConstraintName 在模型生成 API 中更改这一点。 例如:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasForeignKey(e => e.BlogId)
        .HasConstraintName("My_BlogId_Constraint");
}

提示

EF 运行时不使用约束名称。 它仅在使用 EF Core 迁移创建数据库架构时使用。

外键索引

根据约定,EF 为外键的一个或多个属性创建数据库索引。 有关按约定创建的索引类型的详细信息,请参阅模型生成约定

提示

EF 模型中定义了该模型中包含的实体类型之间的关系。 某些关系可能需要在不同上下文的模型中引用实体类型,例如,在使用 BoundedContext 模式时。 在这种情况下,应将外键列映射到普通属性,然后可以手动操作这些属性来处理对关系所做的更改。

主体键

按照约定,外键被约束为关系主体端的主键。 不过,可以改用备用键。 这是在模型生成 API 上使用 HasPrincipalKey 来实现的。 例如,对于单个属性外键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => e.AlternateId);
}

或者对于具有多个属性的组合外键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey(e => new { e.AlternateId1, e.AlternateId2 });
}

HasPrincipalKey 还可以将备用键属性的名称作为字符串传递。 例如,对于单个属性键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey("AlternateId");
}

或者,对于组合键:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasMany(e => e.Posts)
        .WithOne(e => e.Blog)
        .HasPrincipalKey("AlternateId1", "AlternateId2");
}

注意

主体和外键中的属性顺序必须匹配。 这也是在数据库架构中定义键的顺序。 它不必与实体类型或表中列的属性顺序相同。

无需调用 HasAlternateKey 来定义主体实体上的备用键;当 HasPrincipalKey 与不是主键属性的属性一起使用时,会自动执行此操作。 但是,HasAlternateKey 可用于进一步配置备用键,例如设置其数据库约束名称。 有关详细信息,请参阅

与无键实体的关系

每个关系都必须有一个外键,用于引用主体(主要或备用)键。 这意味着无键实体类型不能充当关系的主体端,因为外键没有可引用的主体键。

提示

实体类型可以没有备用键,但不能没有主键。 在这种情况下,备用键(如果有多个,则取其中一个备用键)必须提升为主键。

但是,无键实体类型仍然可以定义外键,因此可以充当关系的依赖端。 例如,请考虑以下类型,其中 Tag 没有键:

public class Tag
{
    public string Text { get; set; } = null!;
    public int PostId { get; set; }
    public Post Post { get; set; } = null!;
}

public class Post
{
    public int Id { get; set; }
}

Tag 可以在关系的依赖端进行配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Tag>()
        .HasNoKey();

    modelBuilder.Entity<Post>()
        .HasMany<Tag>()
        .WithOne(e => e.Post);
}

注意

EF 不支持指向无键实体类型的导航。 请参阅 GitHub 问题 #30331

多对多关系中的外键

多对多关系中,外键在联接实体类型上定义,并映射到联接表中的外键约束。 上述所有内容也可以应用于这些联接实体外键。 例如,设置数据库约束名称:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasMany(e => e.Tags)
        .WithMany(e => e.Posts)
        .UsingEntity(
            l => l.HasOne(typeof(Tag)).WithMany().HasConstraintName("TagForeignKey_Constraint"),
            r => r.HasOne(typeof(Post)).WithMany().HasConstraintName("PostForeignKey_Constraint"));
}