关系 Relationships
关系定义了两个实体类如何相互关联。在关系型数据库中,由外键表示。
1、定义
在描述关系的时候,有许多术语:
- 依赖实体(相关实体,Dependent entity)。这是包含外键属性的实体。又被称为关系的‘子级’。
- 主体实体(Principal entity)。这是包含主/备键属性的实体。又被称为关系的‘父项’。
- 主体秘钥(Principal key)。唯一标识主体实体的属性。可能是主键或者备用秘钥。
- 外键(Foreign key)。用于存储相关实体的主体键值的依赖实体中的属性。
- 导航属性(navigation property)。在主体实体或(和)依赖实体上定义的属性,该属性引用相关实体
- 集合导航属性:包含对多个相关实体的引用。
- 引用导航属性:保存对单个相关实体的引用。
- 反向导航属性:当讨论某个特定的导航属性时,这个术语指的是关系另一端的导航属性。
- 自引用关系。依赖实体和主体实体的类型相同的关系。
下面的示例,展示了Blog和Post两者之间的一对多的关系:
//Post 是依赖实体 public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } //外键 public int BlogId { get; set; } //引用导航属性。是Blog.Posts的逆导航属性(反之亦然) public Blog Blog { get; set; } } //Blog是主体实体 public class Blog { //是主键 public int BlogId { get; set; } public string Url { get; set; } //集合导航属性 public List<Post> Posts { get; set; } }
2、约定
什么是导航属性?如果当前数据库提供程序,无法将其指向的类型映射为标量类型(scalar type,比如int、string、datetime、GUID等),则该属性被视为导航属性。
约定,当在某个类型上发现导航属性时,将创建一个关系。
注意,按约定发现的关系将始终以主体实体的主键为目标。若要以备用秘钥为目标,则必须使用Fluent API执行手动配置。
(本文介绍的内容,主要是基于一对多的关系。本文的最后会专门讨论一对一和多对多的关系)
定义关系的几种模式:
1、完全定义的关系
最常见的模式是在关系两端定义导航属性,在依赖实体中定义外键属性:
-
- 如果在两个类型之间找到一对导航属性,则这些属性将配置为同一关系的反向导航属性
- 如果依赖实体包含名称与其中一种模式相匹配的属性,则该属性将被配置为外键:
- <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; } }
2、无外键属性
虽然建议在依赖实体中定义外键属性,但这并不是必须的。
如果依赖类型没有外键属性,则会使用名称引入名为<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,因为在导航名之前加上前缀是多余的。
注意:如果具有相同名称的属性已经存在,则影子属性名称将添加一个数字作为后缀。
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; } }
4、限制
如果在两种实体类型之间定义了多个导航属性(即不止一对相互指向的导航),则导航属性表示的关系是不明确的。您需要手动配置它们来解决不确定性。
5、级联删除
按照约定,级联删除会为必需关系设置为Cascade, 为可选关系设置为ClientSetNull。Cascade意味着相关实体也被删除。ClientSetNull意味着没有加载到内存中的依赖实体将保持不变,必须手动删除或更新,以指向有效的主体实体。对于加载到内存中的实体,EF Core将尝试将外键属性设置为null。
3、手动配置
配置关系有两种方式。
Fluent API:首先要标识构成关系的导航属性。HasOne或HasMany标识要开始配置的实体类型上的导航属性。然后用一个或多个链接调用来标识逆向导航。HasOne/WithOne用于引用导航属性,HasMany/WithMany用于集合导航属性:
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; } }
数据批注:通常在两个实体类型之间存在多个导航属性对时执行:
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]通常会被忽略,但它可能会导致实体成为依赖实体。
1、手动配置单个导航属性
如果你只有一个导航属性,那么就会有WithOne和WithMany的无参数重载。这表明在关系的另一端存在概念上的引用或集合,但实体类中不包含导航属性:
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; } }
2、手动配置导航属性
这是EF Core 5 新特性
创建导航属性后,你可能需要对其进行进一步配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasMany(b => b.Posts) .WithOne(); modelBuilder.Entity<Blog>() .Navigation(b => b.Posts) .UsePropertyAccessMode(PropertyAccessMode.Property); }
此调用不能用于创建导航属性。 它仅用于配置导航属性。
3、手动配置外键
示例中,分别演示了使用Fluent API来配置应该将哪个属性用作给定关系的外键(组合外键)属性,以及在约定外键属性未被发现时,使用数据注释来配置应该将哪个属性用作给定关系的外键属性:
//使用Fluent API来配置应该将哪个属性用作给定关系的外键属性
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来配置应该将哪个属性用作给定关系的组合外键属性
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; } }
//数据注释来配置应该将哪个属性用作给定关系的外键属性
//可以将[ForeignKey]注释放置在关系中的导航属性上。它不需要进入依赖实体类中的导航属性。在导航属性上使用[ForeignKey]指定的属性不需要存在于依赖类型上。在这种情况下,将使用指定的名称来创建影子外键。
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; } }
4、手动配置阴影外键
可以使用的字符串重载将 HasForeignKey(...)
影子属性配置为外键 (参阅 阴影属性 ) 。 建议先将影子属性显式添加到模型中,然后再将其用作外键 (如下) 所示:
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; } }
5、手动配置外键的约束名称
按照约定,以关系数据库为目标时,外键约束命名为 FK _ <dependent type> _ <principal type> _ <foreign key property> 。 对于复合外键, <foreign key property> 将成为外键属性名称的下划线分隔列表。
你还可以自定义配置约束名称,如下所示:
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"); }
6、手动配置无导航属性
不一定需要提供导航属性。 您可以直接在关系的一端提供外键:
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; } }
7、手动配置主体秘钥
如果希望外键引用主键以外的属性,可以使用Fluent API为关系配置主键属性。您配置为主键的属性将自动设置为备用键(点击查看备用键)。
简单键:
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; } }
组合键:
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; } }
定义组合键的重点:指定主体键属性的顺序必须与为外键指定这些属性的顺序一致。
8、手动配置必须和可选的关系
您可以使用Fluent API配置该关系是必需的还是可选的。它最终控制外键属性是必需的还是可选的。当您使用“影子状态”外键时,这非常有用。如果实体类中有外键属性,那么关系的需求将根据外键属性是必需的还是可选的来确定(有关更多信息,请参阅必选属性和可选属性)。
外键属性位于依赖实体类型上,因此如果用IsRequired配置外键属性,则意味着每个依赖实体都需要有一个对应的主体实体。调用IsRequired(false)会使外键属性成为可选属性。
9、手动配置级联删除
您可以使用Fluent API显式地为给定关系配置级联删除行为。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .OnDelete(DeleteBehavior.Cascade); }
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的泛型参数。在一对多关系中,很明显,具有引用导航的实体是从属实体,具有集合的实体是主体实体。但在一对一关系中并不是这样(因为主体中已经没有集合了)—因此需要显式地定义它:
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不会验证是否提供了依赖实体,因此,只有在数据库映射允许强制执行时,该配置才会有所不同。这方面的一个常见场景是默认使用表分割的引用拥有类型(reference owned types):
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 的新特性。
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; } }
在数据库中实现这种关系的方式是通过一个连接表,其中包含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创建了一个实体类型来表示连接表(join table),该连接表将被称为连接实体类型(join entity type)。没有特定的CLR类型可以用于此,所以使用Dictionary<string, object>。模型中可以存在多个多对多关系,因此必须为联接实体类型指定一个惟一的名称,在本例中为PostTag。允许这样做的特性称为共享类型实体类型(shared-type entity type)。
多对多导航称为跳过导航(skip navigations),因为它们有效地跳过了联接实体类型。如果你正在使用批量配置,所有跳过导航都可以从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类型。在使用自定义联接实体类型配置关系时,需要显式指定两个外键:
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 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; } }
目前尚不支持对来自数据库多对多关系的搭建支持。