关系
关系定义两个实体之间的相关性。 在关系数据库中,关系由外键约束表示。
注意
本文中的大多数示例都使用一对多关系来演示概念。 有关一对一和多对多关系的示例,请参阅本文末尾的“其他关系模式”部分。
术语的定义
有许多用于描述关系的术语
依赖实体:这是包含外键属性的实体。 有时是指关系的“子级”。
主体实体:这是包含主键/备选键属性的实体。 有时是指关系的“父级”。
主体键:唯一标识主体实体的属性。 它可能是主键,也可能是备选键。
外键:依赖实体中用于存储相关实体的主体键值的属性。
导航属性:在引用相关实体的主体实体和/或依赖实体上定义的属性。
集合导航属性:包含对许多相关实体的引用的导航属性。
引用导航属性:保留对单个相关实体的引用的导航属性。
反向导航属性:讨论特定导航属性时,此术语是指关系另一端上的导航属性。
自引用关系:依赖实体类型与主体实体类型相同的关系。
以下代码展示了 Blog
和 Post
之间的一对多关系
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Post
是依赖实体Blog
是主体实体Blog.BlogId
是主体键(在此示例中,它是主键,而不是备选键)Post.BlogId
是外键Post.Blog
是引用导航属性Blog.Posts
是集合导航属性Post.Blog
是Blog.Posts
的反向导航属性(反之亦然)
约定
默认情况下,如果在类型上发现导航属性,将创建关系。 如果它指向的类型不能由当前数据库提供程序映射为标量类型,则属性被视为导航属性。
注意
按约定发现的关系将始终以主体实体的主键为目标。 若要以备选键为目标,必须使用 Fluent API 执行配置。
完全定义的关系
关系的最常见模式是在关系的两端定义导航属性,并在依赖实体类中定义外键属性。
如果在两种类型之间找到了一对导航属性,则它们将被配置为相同关系的反向导航属性。
如果依赖实体包含一个名称与以下模式之一匹配的属性,则该属性将被配置为外键:
<navigation property name><principal key property name>
<navigation property name>Id
<principal entity name><principal key property name>
<principal entity name>Id
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
本示例将使用突出显示的属性来配置关系。
注意
如果属性是主键或属性的类型与主体键不兼容,则不会将其配置为外键。
无外键属性
虽然建议在依赖实体类中定义外键属性,但这不是必需的。 如果未找到外键属性,则将使用名称 <navigation property name><principal key property name>
或 <principal entity name><principal key property name>
引入外键属性(如果依赖类型上不存在导航)。
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
此示例中,影子外键为 BlogId
,因为预先输入导航名称是多余的。
注意
如果已存在同名的属性,则影子属性名称将带有数字后缀。
单个导航属性
仅包含一个导航属性(没有反向导航,也没有外键属性)就足以按约定定义关系。 还可包含一个导航属性和一个外键属性。
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
限制
如果在两个类型之间定义了多个导航属性(即不止一对指向彼此的导航),则由导航属性表示的关系是不明确的。 需要对它们进行手动配置才能解决这种不明确的关系。
级联删除
根据约定,对于必需关系,级联删除将设置为 Cascade,对于可选关系,它将设置为 ClientSetNull。 Cascade 表示也会删除依赖实体。 ClientSetNull 表示未加载到内存中的依赖实体将保持不变,必须手动删除或更新以指向有效的主体实体。 对于加载到内存中的实体,EF Core 将尝试将外键属性设置为 null。
有关必需关系和可选关系的区别,请参阅必需关系和可选关系部分。
有关约定使用的不同删除行为和默认值的更多详细信息,请参阅级联删除。
手动配置
若要在 Fluent API 中配置关系,首先应标识构成关系的导航属性。 HasOne
或 HasMany
标识要开始配置的实体类型的导航属性。 然后,将调用链接到 WithOne
或 WithMany
以标识反向导航。 HasOne
/WithOne
用于引用导航属性,HasMany
/WithMany
用于集合导航属性。
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
单个导航属性
如果只有一个导航属性,则 WithOne
和 WithMany
会发生无参数重载。 这表示在关系的另一端,存在概念上的引用或集合,但实体类中不包含导航属性。
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
配置导航属性
注意
EF Core 5.0 中已引入此功能。
创建导航属性后,可能需要进一步对其进行配置。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();
modelBuilder.Entity<Blog>()
.Navigation(b => b.Posts)
.UsePropertyAccessMode(PropertyAccessMode.Property);
}
注意
此调用不能用于创建导航属性。 它仅用于配置之前通过定义关系或通过约定创建的导航属性。
外键
可使用 Fluent API 来配置哪个属性应用作给定关系的外键属性:
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogForeignKey);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}
影子外键
可以使用字符串重载 HasForeignKey(...)
将阴影属性配置为外键, (请参阅 阴影属性 以获取详细信息) 。 建议在将影子属性用作外键之前将其显式添加到模型中(如下所示)。
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Add the shadow property to the model
modelBuilder.Entity<Post>()
.Property<int>("BlogForeignKey");
// Use the shadow property as a foreign key
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey("BlogForeignKey");
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
外键约束名称
根据约定,当以关系数据库作为目标时,外键约束将命名为 FK__<依赖类型名称>_<主体类型名称>_<外键属性名称>。 对于复合外键,<外键属性名称> 将成为外键属性名称的下划线分隔列表。
还可配置约束名称,如下所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.HasConstraintName("ForeignKey_Post_Blog");
}
没有导航属性
无需提供导航属性。 只需在关系的一侧提供外键。
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne<Blog>()
.WithMany()
.HasForeignKey(p => p.BlogId);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
}
主体键
如果想要外键引用主键外的属性,可使用 Fluent API 为关系配置主体键属性。 配置为主体键的属性将自动设置为备选键。
internal class MyContext : DbContext
{
public DbSet<Car> Cars { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RecordOfSale>()
.HasOne(s => s.Car)
.WithMany(c => c.SaleHistory)
.HasForeignKey(s => s.CarLicensePlate)
.HasPrincipalKey(c => c.LicensePlate);
}
}
public class Car
{
public int CarId { get; set; }
public string LicensePlate { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public List<RecordOfSale> SaleHistory { get; set; }
}
public class RecordOfSale
{
public int RecordOfSaleId { get; set; }
public DateTime DateSold { get; set; }
public decimal Price { get; set; }
public string CarLicensePlate { get; set; }
public Car Car { get; set; }
}
必需关系和可选关系
可使用 Fluent API 来配置关系是必需还是可选的。 此操作将最终控制外键属性是必需还是可选的。 这在使用影子状态外键时最有用。 如果实体类中具有外键属性,则关系的必需性取决于外键属性是必需还是可选(有关详细信息,请参阅必需和可选属性)。
外键属性位于依赖实体类型上,因此如果按要求配置它们,则意味着每个依赖实体都需要具有相应的主体实体。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}
注意
除非另有配置,否则调用 IsRequired(false)
也会使外键属性为可选。
级联删除
可使用 Fluent API 显式配置给定关系的级联删除行为。
有关每个选项的详细介绍,请参阅级联删除。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.OnDelete(DeleteBehavior.Cascade);
}
其他关系模式
一对一
一对一关系在两侧都有引用导航属性。 它们遵循与一对多关系相同的约定,但在外键属性上引入了一个唯一索引,以确保只有一个依赖项与每个主体相关。
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public BlogImage BlogImage { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
注意
EF 将基于其检测外键属性的能力,选择其中一个实体作为依赖实体。 如果选择错误的实体作为依赖项,可使用 Fluent API 来更正此错误。
在配置与 Fluent API 的关系时,可使用 HasOne
和 WithOne
方法。
配置外键时,需要指定依赖实体类型 - 请注意下面列表中提供给 HasForeignKey
的泛型参数。 显而易见,在一对多关系中,具有引用导航的实体是依赖项,而具有集合的实体是主体。 但在一对一的关系中并非如此,因此需要对其进行显式定义。
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public BlogImage BlogImage { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }
public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}
默认情况下依赖端为可选,但可视需要进行配置。 但是,EF 不会验证是否提供了依赖实体,因此此配置仅在数据库映射允许强制执行时才会产生影响。 此种情况的一个常见场景是默认使用表拆分的引用从属类型。
modelBuilder.Entity<Order>(
ob =>
{
ob.OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).IsRequired();
sa.Property(p => p.City).IsRequired();
});
ob.Navigation(o => o.ShippingAddress)
.IsRequired();
});
通过此配置,与 ShippingAddress
对应的列将在数据库中标记为不可为 null。
注意
如果使用不可为 null 的引用类型,则不需要调用 IsRequired
。
注意
EF Core 5.0 中引入了配置是否需要依赖项的功能。
多对多
多对多关系需要双方的集合导航属性。 与其他类型的关系一样,它们也可通过约定发现。
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
}
在数据库中实现此关系的方式是使用联接表,其中包含 Post
和 Tag
的外键。 例如,以下就是 EF 将在关系数据库中为上述模型创建的内容。
CREATE TABLE [Posts] (
[PostId] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
);
CREATE TABLE [Tags] (
[TagId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
);
CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
CONSTRAINT [FK_PostTag_Posts_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);
在内部,EF 创建一个实体类型来表示将称为联接实体类型的联接表。 Dictionary<string, object>
当前用于处理外键属性的任意组合,有关详细信息,请参阅属性包实体类型。 模型中可以存在多个多对多关系,因此必须为联接实体类型指定唯一的名称,在本例中为 PostTag
。 允许此操作的功能称为共享类型实体类型。
重要
按照约定,用于联接实体类型的 CLR 类型可能会在未来版本中更改以提高性能。 除非已显式配置,否则不要依赖于联接类型 Dictionary<string, object>
,如下一节所述。
多对多导航称为跳过导航,因为它们有效地跳过联接实体类型。 如果要使用批量配置,则可以从 GetSkipNavigations中获取所有跳过导航。
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var skipNavigation in entityType.GetSkipNavigations())
{
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
}
}
联接实体类型配置
将配置应用于联接实体类型是很常见的。 此操作可通过 UsingEntity
完成。
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
可使用匿名类型为联接实体类型提供模型种子数据。 可检查模型调试视图,以确定按约定创建的属性名称。
modelBuilder
.Entity<Post>()
.HasData(new Post { PostId = 1, Title = "First" });
modelBuilder
.Entity<Tag>()
.HasData(new Tag { TagId = "ef" });
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.HasData(new { PostsPostId = 1, TagsTagId = "ef" }));
其他数据可存储在联接实体类型中,但为此最好创建一个定制的 CLR 类型。 使用自定义联接实体类型配置关系时,需要显式指定这两个外键。
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
联接关系配置
EF 对联接实体类型使用两个一对多关系来表示多对多关系。 可在 UsingEntity
参数中配置这些关系。
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTag",
j => j
.HasOne<Tag>()
.WithMany()
.HasForeignKey("TagId")
.HasConstraintName("FK_PostTag_Tags_TagId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne<Post>()
.WithMany()
.HasForeignKey("PostId")
.HasConstraintName("FK_PostTag_Posts_PostId")
.OnDelete(DeleteBehavior.ClientCascade));
注意
配置多对多关系的功能是在 EF Core 5.0 中引入的,对于以前的版本,请使用以下方法。
间接多对多关系
还可表示多对多关系,只需添加联接实体类型并映射两个单独的一对多关系。
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(t => new { t.PostId, t.TagId });
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
注意
尚未添加对数据库中多对多关系搭建基架的支持。 请参阅跟踪问题。
其他资源
- .NET 数据Community站立会话,深入了解多对多和基础结构的基础。