关系中的外键和主键
所有一对一和一对多关系均由依赖端上的外键所定义,用于引用主体端上的主键或备用键。 为方便起见,此主键或备用键称为关系的“主键”。 多对多关系由两个一对多关系组成,每个关系本身由引用主键的外键所定义。
提示
可以在 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"));
}