所有 一对一 关系和 一对多 关系都由引用主体端的主键或备用键的依赖端的外键定义。 为方便起见,此主键或备用键称为关系的“主体密钥”。 多对多 关系由两个一对多关系组成,每个关系本身通过外键引用主体键来定义。
小窍门
可在 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"));
}