文章目录
关系
关系(relationship)定义了两个实体(entity)之间是怎样联系的。
在关系数据库中,这是由外键约束(foreign key constraint)来表示的。
注意:
本文中的大多数示例使用一对多的关系来说明概念。关于一对一的关系很简单,多对多关系的示例可以自行研究。
1. 术语的定义
用来描述关系的术语有许多:
这些术语对初学者来讲很头疼,但没办法,因为数据库这个东西,尤其是关系数据库,背后是有理论模型的,还有数学概念支撑。而这些理论模型、数学概念就是抽象的,它必然会产生一套概念、术语去描述它。所以在深入关系之前,有必要对这些概念有个大致了解。
关系数据库模型中有许多概念与EF Core中类似,甚至等同。但是我学习的时候发现它们的英文是不一样的,比如关系数据库中主键是primary key,而EF Core中有principal key,两者虽然看似相等,但实际上不完全是一个东西。
我想,作为初学者,应该谨慎对待。比如principal key,从微软文档来看,可能是主键或者候补键,那就不能和主键等同。
- 依赖实体(Dependent entity):也叫从实体或子实体,这是一个包含外键属性的实体。有时也被称为关系的孩子。我个人喜欢叫它从实体,因为还有主实体,主从嘛,比较好记。
- 主体实体(Principal entity):也叫主实体,这个实体包含了主键/候补键属性。有时候也称其为关系的父亲。
这对实体结合父子、主从的概念来看都是很好理解的。从/子实体通过外键来找到主/父实体,进而访问主/父实体。因为从实体中的外键属性值,在主实体中必然存在,且为键。
- 主体键(Principal key):这是一个唯一标识了主实体的属性。它可能是主键或候补键。我之所以称它为主体键,因为它是用来标识主实体的,而且这样命名可以与主键做区分。
- 外键(Foreign key):它是存在于从实体中的属性,用于存储相关联实体(主实体)的主体键的值。
外键:如果一个实体的某个字段指向另一个实体的主键,就称为外键。被指向的实体,称之为主实体,也叫父实体。负责指向的实体,称之为从实体,也叫子实体。从名字上也不难理解,外键就是外部(其他)实体的键。
- 导航属性(Navigation property):这是一个定义在主实体或从实体中来引用相关实体的的属性。
比如,我的博客中有许多文章,点击文章链接,就可以跳转到文章;又比如,我的文章中有标注所属的博客,我点击博客又可以跳转到我的博客信息。导航属性其实很简单,就是可以通过它跳转到别的实体上。就像导航栏的功能一样。
- 导航属性还可细分为三种属性,
- 集合导航属性(Collection navigation property):它是一个包含对许多相关实体引用的导航属性。(往往是一个集合,它引用了不止单个实体)
- 引用导航属性(Reference navigation property):持有对单个相关实体的引用的导航属性。
- 逆导航属性(Inverse navigation property):在讨论一个特定的导航属性时,这个术语指的是该关系另一端的导航属性。
- 集合导航属性可以认为就是一个容器,容器中装了多个导航属性。
- 引用导航属性就是最基本的导航属性,就是单个引用。
- 逆导航属性就是该关系另一端的实体中的导航属性。比如,人和苹果是一对实体,人吃苹果,吃是一种关系。针对人吃苹果这个关系,人的实体中可以访问苹果,那苹果也可以访问人,苹果对于人的关系就是被吃。关键在于人吃苹果这个特定的关系。
- 自参照关系(Self-referencing relationship):依赖实体类型和主要实体类型相同的关系。
以下代码展示了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的相反导航属性(反之亦然)。
2. 约定(即按照EF Core默认规定发现生成关系)
默认情况下,当在类型(指实体类)上发现导航属性时,就会创建关系。如果当前数据库提供程序无法将属性指向的类型映射为值类型(标量类型,scalar type),则该属性就被视为导航属性。(关系模型的域这个概念中有说到,域中元素,即属性的取值范围里的值都应该是原子的,而这边发现属性指向类型还能继续分,说明不符合域的原子性的约束,所以被数据库提供程序视为了导航属性)
所以按照约定,关系是在导航属性被发现的情况下被创建的。
注意:
按照约定发现的关系将始终以主实体的主键为目标。
要以候补键为目标,必须使用fluent API执行额外的配置。
2.1. 完全定义的关系
关于关系,最常用的模式是在关系的两端都定义导航属性,并在从/依赖实体类中定义外键属性。
- 如果在两个类型之间发现了一对导航属性,那么他们将配置为具有相同关系的逆导航属性。
- 如果从实体包含了一个属性,该属性名与下面模式中的某个相匹配,则它被配置为外键:
- <navigation property name><principal key property name>
<导航属性名> + <主体键属性名> - <navigation property name>Id
<导航属性名> + Id - <principal entity name><principal key property name>
<主实体名> + <主体键属性名> - <principal entity name>Id
<主实体名> + Id
- <navigation property name><principal key property name>
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
// 1 集合导航属性
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
// 2 外键
public int BlogId { get; set; }
// 3 导航属性
public Blog Blog { get; set; }
}
本例中,标注的属性将被用于配置关系。
注意:
如果该属性是主键或其类型与主体键不兼容,则它不会被配置为外键。
2.2. 无外键属性(的关系)
尽管建议在从实体类中定义外键属性,但这不是必须的。如果没有找到外键属性,将会引入影子外键属性(shadow foreign key property),其名称为:
- <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,因为前缀增加导航名显然是多余的(加了就变成了BlogBlogId)。
注意:
如果相同名称的属性已经存在,则影子属性名会加上数字后缀。
2.3. 单个导航属性
只包含一个导航属性(没有相反导航属性也没有外键属性)就足以由约定来定义关系。当然,你还可以添加导航属性和外键属性。
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; }
}
2.4. 局限性
当在两个类型之间定义了多个导航属性时(就是说,不止一对导航指向彼此),由导航属性表示的关系比较模糊。你将需要手动配置它们来解决歧义。
2.5. 级联删除(Cascade delete)
按照约定,对于必需(required)的关系,级联删除将被设置为Cascade。对于可选的关系,设置为ClientSetNull。
Cascade意味着从实体也被删除。
ClientSetNull表示没有加载到内存的从实体将保持不变,必须手动删除,或更新以指向有效的主体实体。对于加载到内存中的实体,EF Core将尝试将外键属性设置为空。
3. 手动配置
手动是相对于约定的,约定是你按照EF Core的命名习惯去命名,EF Core会自己识别关系。
而手动是你自己显式地指明关系。
手动配置有两种方式,Fluent API和数据标注(Data annotations)。
- Fluent API:
// Fluent API
// 要在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>() // 获取模型中的Post实体
.HasOne(p => p.Blog) // has,have有让、使的意思,可以理解为让某个属性成为导航属性
.WithMany(b => b.Posts);// With有用的意思,可以理解为用某个属性指向该实体
// 至于One和Many,那就是数量上的区别了,引用和集合的区别
}
}
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; }
}
- 数据标注:
// 你也可以使用数据标注来配置在从实体和主实体上导航属性配对的方式
// 在两个实体之间有超过一对的导航属性时,通常会这么做。
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int AuthorUserId { get; set; }
public User Author { get; set; }
public int ContributorUserId { get; set; }
public User Contributor { get; set; }
}
public class User
{
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[InverseProperty("Author")]
public List<Post> AuthoredPosts { get; set; }
[InverseProperty("Contributor")]
public List<Post> ContributedToPosts { get; set; }
}
注意:
你只能在从实体的属性上使用[Required]来影响关系的必需性。
主实体导航属性上的[Required]通常被忽略(主体键本来就不能为空),但它可能导致实体变成依赖实体。
注意:
数据标注[ForeignKey]和[InverseProperty]在命名空间System.ComponentModel.DataAnnotations.Schema中可用。
[Required]在命名空间System.ComponentModel.DataAnnotations中可用。
3.1. 单个导航属性
如果你只有一个导航属性,那么就可以用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; }
}
3.2. 配置导航属性
注意:
EF Core 5.0中引入的该特性。
在创建导航属性后,你需要进一步配置它。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>() // 从模型中获取Blog实体
.HasMany(b => b.Posts) // 让Posts成为集合导航属性
.WithOne(); // 用主实体中的逆导航属性指向Blog实体
modelBuilder.Entity<Blog>()
.Navigation(b => b.Posts) // 获取Blog实体中的Posts导航属性
.UsePropertyAccessMode(PropertyAccessMode.Property);
}
注意:
此调用无法用于创建导航属性。它只用于配置导航属性,该属性是先前通过定义关系或根据约定创建的。(也就是说你得用其他方法先创建了导航属性,才能配置它)
3.3. 外键
- Fluent API(单键)
// Fluent API (单键)
// 你可以使用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); // 将Post中的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; }
}
- Fluent API(组合键)
// 用Fluent API来配置给定关系的用作组合键的属性
internal class MyContext : DbContext
{
public DbSet<Car> Cars { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => new { c.State, c.LicensePlate });
modelBuilder.Entity<RecordOfSale>()
.HasOne(s => s.Car)
.WithMany(c => c.SaleHistory)
.HasForeignKey(s => new { s.CarState, s.CarLicensePlate });
}
}
public class Car
{
public string State { 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 CarState { get; set; }
public string CarLicensePlate { get; set; }
public Car Car { get; set; }
}
- 数据标注(单键)
// 你可以使用数据标注来配置给定关系中用作外键的属性。
// 当按约定未发现外键属性时,通常会这么做:
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; }
[ForeignKey("BlogForeignKey")]
public Blog Blog { get; set; }
}
提示:
[ForeignKey]标注可以放在关系中的任意导航属性上。
它不需要在从(依赖)实体类中的导航属性上。
注意:
在导航属性上使用[ForeignKey]指定的属性不需要存在于从实体类(依赖类)中。
不存在的情况下,会创建指定的名称的影子外键。
3.4. 影子外键
你可以使用 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; }
}
3.5. 外键约束名称
按照惯例,当目标为关系数据库时,外键约束名称为
FK_<dependent type name><principal type name><foreign key property name>,
即FK_从实体类名_主实体类名_外键属性名。
对于组合外键,<foreign key property name>会变成一个下划线分割的外键属性名称表。
你也可以按照以下方式来配置约束名:
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");
}
3.6. 不带导航属性
你也不是一定要提供导航属性,你可以简单地在关系的另一端提供一个外键。
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; }
}
3.7. 主体键
如果希望外键引用主键(primary key)以外的属性,可以使用Fluent API来配置关系的主体键(principal key)属性。配置为主体键的属性将自动设置为候补键。
- 单键的情况,Simple key
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; }
}
- 组合键的情况,Composite key
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 => new { s.CarState, s.CarLicensePlate })
.HasPrincipalKey(c => new { c.State, c.LicensePlate });
}
}
public class Car
{
public int CarId { get; set; }
public string State { 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 CarState { get; set; }
public string CarLicensePlate { get; set; }
public Car Car { get; set; }
}
警告:
指定主体键属性的顺序必须与外键指定的顺序匹配。
3.8. 必需的和可选的关系
可以使用Fluent API来配置一个关系是必需的还是可选的。最终,这将会控制外键属性是必需的还是可选的。当使用影子外键时,这非常有用。如果你的实体类中有一个外键属性,那么关系的需要性取决于外键属性是必需的还是可选的。
外键属性位于依赖实体类型上,因此如果它们被配置为必需的,就意味着每个从实体都需要一个对应的主实体。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}
注意:
调用IsRequired(false)也会使外键属性变成可选的,除非在其它地方配置了。
3.9. 级联删除
使用Fluent API来为给定的关系显式配置级联删除行为。
关于级联删除的详细信息,可以参阅级联删除章节Cascade delete。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.OnDelete(DeleteBehavior.Cascade);
}
4. 其它关系模式
4.1. 一对一
一对一关系在关系两边都有一个引用导航属性。它们遵循与一对多关系相同的约定,但是在外键属性上引入了唯一的索引来确保只有一个从实体与每个主实体相关。
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; }
}
默认情况下,从/依赖实体侧被认为是可选的(optional),不过可以根据需要配置为必需的(required)。但是,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对应的列将在数据库中标记为不可为空。
注意:
如果你使用了不可为空引用类型,那调用IsRequired就不是必要的。
注意:
配置从实体是否必需的功能是EF Core 5.0引入的。
4.2. 多对多
多对多关系需要关系两端都有集合导航属性。它们会像其他关系类型一样被默认约定发现。
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; }
}
在数据库中实现这种关系的方式是使用一张连接表(join table),该表包含了指向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>;除非你已经显式地配置了它。//??
多对多导航被称为跳过(skip)导航,因为它们能有效地跳过连接实体类型。如果你使用批量配置,所有跳过导航都可以从GetSkipNavigations获得。
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var skipNavigation in entityType.GetSkipNavigations())
{
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
}
}
4.2.1. 连接实体类型配置
给连接实体类型应用配置是很常见的,这个操作可通过UsingEntity来完成。
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
可以使用匿名类型来为连接实体类型提供模型种子数据(model seed data)。你可以检测模型调试视图来确认由约定创建的属性名。
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; }
}
4.2.2. 连接关系配置
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引入的。之前的版本使用的是以下方法。
4.2.3. 间接的多对多关系
你也可以通过添加连接实体类型和映射两个独立的一对多关系来代表多对多关系。
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; }
}
注意:
支持对数据库搭建多对多关系的功能还未添加。