https://docs.microsoft.com/zh-cn/ef/core/modeling/relationships
文章目录
1. 术语介绍
- Dependent entity: 依赖实体,包含外键的实体,实体关系中的子。
- Principal entity: 主体实体,包含主键/备用键的实体,实体关系中的父。
- Principal key: 主键/备用键
- Foreign key: 外键
-
Navigation property: 导航属性,出现在Dependent entity 或Principal entity上的实体,用来指向相关的其它实体。
- Collection navigation property:是多个对象的集合
- Reference navigation property:单个对象
- Inverse navigation property:反向导航属性
- Self-referencing relationship:自引用的关系,如树形结构中的id和parentId
参考以下代码:
public class Blog//主体实体
{
public int BlogId { get; set; }//主键
public string Url { get; set; }
public List<Post> Posts { get; set; }//导航属性,也是Post.Blog的反向导航属性
}
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; }//导航属性,也是Blog.Posts的反向导航属性
}
2. 自动配置的关系
如果实体类的一个属性无法映射为标量类型(scalar type, 如int、long等),则认为是导航属性。如果在某个实体类上发现了导航属性,将会创建一个关系。
2.1 完整配置
如上一小节的Post和Blog的定义,都是像那样定义比较完备的。定义完备有两点要求:
- 实体两边有对应的导航属性,而非一边有一边没有
- 如果“依赖实体”中某个属性的名称符合以下规则,则被配置为外键:
<navigation property name><principal key property name>
<navigation property name>Id
<principal entity name><principal key property name>
<principal entity name>Id
2.2 无外键情况下进行配置
虽然建议要有外键,但是没有外键也ok。当没外键时会引入名称符合<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
的影子属性。
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; }
}
3. 手动配置关系
使用HasOne/HasMany
配置导航属性和使用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<Post>()
.HasOne(p => p.Blog)//表示一个Post对应一个Blog
.WithMany(b => b.Posts);//表示一个Blog对应多个Post
//上述代码等效于下面这两行
modelBuilder.Entity<Post>().HasOne(p=>p.Blog);
modelBuilder.Entity<Blog>().HasMany(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; }
}
看下建表语句都是什么:
CREATE TABLE "SysRole" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_SysRole" PRIMARY KEY AUTOINCREMENT,
"RoleName" TEXT NOT NULL
);
CREATE TABLE "SysUsers" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_SysUsers" PRIMARY KEY AUTOINCREMENT,
"UserName" TEXT NOT NULL,
"Pwd" TEXT NOT NULL,
"CreateTime" TEXT NULL,
"RoleId" INTEGER NULL,
CONSTRAINT "FK_SysUsers_SysRole_RoleId" FOREIGN KEY ("RoleId") REFERENCES "SysRole" ("Id") ON DELETE RESTRICT
);
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 手动指定外键
使用FluentAPI可以配置单一外键和复合外键:
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; }
}
复合外键:
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")]//如果BlogForeignKey不存在,则会创建影子属性
public Blog Blog { get; set; }
}
配置影子外键:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//1.首先配置影子属性
modelBuilder.Entity<Post>()
.Property<int>("BlogForeignKey");
//2.然后将影子属性配置成外键
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey("BlogForeignKey");
}
配置外键的名称:默认情况下外键的名称规则为FK _<依赖实体的类名>_<主体实体的类名>_<外键属性的名称>
可以在数据库中看到,如FK_SysUsers_SysRole_RoleId
。
当然也可以手动更改外键的名称:
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.3 无导航属性下的配置
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.4 将外键指向到非主键上
如果希望外键指向到非实体主键的属性上。则可以如下配置:
- 单一外键
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; }
}
- 复合外键
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 })//CarState, CarLicensePlate的顺序需要和State, LicensePlate对应
.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.5 设置外键可否为空
默认情况下外键可否为空是根据实体类中外键属性可否为空来的。但是你可以通过 以下方式配置影子外键可否为空:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>().HasOne(p => p.Blog).WithMany(b => b.Posts).IsRequired();
}
3.6 设置级联删除
主体实体删除时删除对应的依赖实体
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.OnDelete(DeleteBehavior.Cascade);
}
4. 其他关系的配置
上面的例子我们介绍的都是一对多的关系,接下来介绍其他类型的关系
4.1 一对一
两侧的导航属性都不是集合,在外键属性上有唯一索引。EF会自动判断依赖实体和主体实体。
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; }
//定义了外键,EF认为这个就是依赖实体
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
如果EF自动判断的依赖实体和主体实体出错,则依赖根据之前介绍的使用FluentAPI和特性手动指定。
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; }
}
会额外创建一个关联表:
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
);
当然也可以显示的创建一个实体来定义多对多的关系:
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; }
}