关系中的外键和主键

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

小窍门

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

外键

构成外键的一个或多个属性通常是通过惯例确定的。 还可以使用 映射属性 或在 HasForeignKey 模型生成 API 中显式配置属性。 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");
}

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

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

不能为空的外键列

“可选”和“必需”关系中所述,外键属性的可为 null 性决定了关系是可选的还是必需关系。 但是,可为空的外键属性可以在必需关系中使用,通过 [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();
}

或者,如果外键 是通过约定发现的IsRequired 则可以在不调用以下 HasForeignKey项的情况下使用:

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

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

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

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

隐式外键属性也可以显式创建,这对于配置属性的具体细节非常有用。 例如,若要使属性变为非空:

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

小窍门

按照约定,外键属性从关系中的主键继承特性,例如最大长度和 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> 将成为外键属性名称的下划线分隔列表。

可以在模型构建 API 中使用 HasConstraintName 进行更改。 例如:

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 模式时。 在这些情况下,外键列应映射到普通属性,然后可以手动操作这些普通属性,以处理关系的更改。

主密钥

按照惯例,外键被限制为关系中主要端的主键。 但是,可以改用备用密钥。 通过在HasPrincipalKey模型构建API中使用可以实现这一点。 例如,对于单个属性外键:

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