关系(relationship)定义了两个实体(entity)之间是怎样联系的。
在关系数据库中,这是由外键约束(foreign key constraint)来表示的。
注意:
本文中的大多数示例使用一对多的关系来说明概念。关于一对一的关系很简单,多对多关系的示例可以自行研究。
用来描述关系的术语有许多:
这些术语对初学者来讲很头疼,但没办法,因为数据库这个东西,尤其是关系数据库,背后是有理论模型的,还有数学概念支撑。而这些理论模型、数学概念就是抽象的,它必然会产生一套概念、术语去描述它。所以在深入关系之前,有必要对这些概念有个大致了解。
关系数据库模型中有许多概念与EF Core中类似,甚至等同。但是我学习的时候发现它们的英文是不一样的,比如关系数据库中主键是primary key,而EF Core中有principal key,两者虽然看似相等,但实际上不完全是一个东西。
我想,作为初学者,应该谨慎对待。比如principal key,从微软文档来看,可能是主键或者候补键,那就不能和主键等同。
这对实体结合父子、主从的概念来看都是很好理解的。从/子实体通过外键来找到主/父实体,进而访问主/父实体。因为从实体中的外键属性值,在主实体中必然存在,且为键。
外键:如果一个实体的某个字段指向另一个实体的主键,就称为外键。被指向的实体,称之为主实体,也叫父实体。负责指向的实体,称之为从实体,也叫子实体。从名字上也不难理解,外键就是外部(其他)实体的键。
比如,我的博客中有许多文章,点击文章链接,就可以跳转到文章;又比如,我的文章中有标注所属的博客,我点击博客又可以跳转到我的博客信息。导航属性其实很简单,就是可以通过它跳转到别的实体上。就像导航栏的功能一样。
以下代码展示了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的相反导航属性(反之亦然)。
默认情况下,当在类型(指实体类)上发现导航属性时,就会创建关系。如果当前数据库提供程序无法将属性指向的类型映射为值类型(标量类型,scalar type),则该属性就被视为导航属性。(关系模型的域这个概念中有说到,域中元素,即属性的取值范围里的值都应该是原子的,而这边发现属性指向类型还能继续分,说明不符合域的原子性的约束,所以被数据库提供程序视为了导航属性)
注意:
按照约定发现的关系将始终以主实体的主键为目标。
要以候补键为目标,必须使用fluent API执行额外的配置。
关于关系,最常见的模式是在关系的两端都定义导航属性,并在从实体类中定义外键属性。
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; }
}
本例中,标注的属性将被用于配置关系。
注意:
如果该属性是主键或其类型与主体键不兼容,则它不会被配置为外键。
尽管建议在从实体类中定义外键属性,但这不是必须的。如果没有找到外键属性,将会引入影子外键属性(shadow foreign key property),其名称为:
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)。
注意:
如果相同名称的属性已经存在,则影子属性名会加上数字后缀。
只包含一个导航属性(没有相反导航属性也没有外键属性)就足以由约定来定义关系。当然,你还可以添加导航属性和外键属性。
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; }
}
当在两个类型之间定义了多个导航属性时(就是说,不止一对导航指向彼此),由导航属性表示的关系比较模糊。你将需要手动配置它们来解决歧义。
按照约定,对于必需(required)的关系,级联删除将被设置为Cascade。对于可选的关系,设置为ClientSetNull。
Cascade意味着从实体也被删除。
ClientSetNull表示没有加载到内存的从实体将保持不变,必须手动删除,或更新以指向有效的主体实体。对于加载到内存中的实体,EF Core将尝试将外键属性设置为空。
手动配置有两种方式,Fluent API和数据标注(Data annotations)。
从我个人角度来看,数据标注的方式比较适合新手。不过这边还是两种都介绍下。
// 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中可用。
如果你只有一个导航属性,那么就可以用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 (单键)
// 你可以使用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; }
}
// 用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]指定的属性不需要存在于从实体类(依赖类)中。
不存在的情况下,会创建指定的名称的影子外键。
你可以使用==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; }
}