本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目 实战》,本人记录梳理的学习笔记,有部分的增补和省略。更全面系统的讲解,请看杨老师的视频课:【.NET教程,.Net Core视频教程,杨中科主讲】。
一、关系配置
EF Core 中实体间的关系是业务模型的核心,正确配置一对一、一对多、多对多关系,能保证数据存储的完整性和查询的便捷性。所有关系配置都基于 EntityTypeBuilder(可通过实现 IEntityTypeConfiguration<T> 或 OnModelCreating 方法配置),核心是通过 HasOne/HasMany 声明 “当前实体” 的导航属性,通过 WithOne/WithMany 声明 “关联实体” 的导航属性,形成双向绑定。
1. 一对一关系
一对一关系要求两个实体严格一一对应,必须显式声明外键属性(否则 EF Core 可能无法正确识别关系),且只需在其中一个实体的配置中完成即可。
语法
// 以“用户-用户详情”为例,在UserProfile配置中声明一对一关系
builder.HasOne<User>(p => p.User) // 详情表关联到一个用户(当前实体:UserProfile)
.WithOne(u => u.Profile) // 一个用户对应一个详情(关联实体:User)
.HasForeignKey<UserProfile>(p => p.UserId); // 声明外键(外键所在的实体:UserProfile)
示例
实体类(显式声明外键):
// 用户主表
public class User
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性:关联用户详情
public UserProfile Profile { get; set; }
}
// 用户详情表(外键所在表)
public class UserProfile
{
public int Id { get; set; }
public string Address { get; set; }
// 显式外键属性(必须声明)
public int UserId { get; set; }
// 导航属性:关联用户
public User User { get; set; }
}
配置类:
public class UserProfileConfig : IEntityTypeConfiguration<UserProfile>
{
public void Configure(EntityTypeBuilder<UserProfile> builder)
{
builder.ToTable("T_UserProfiles");
// 配置一对一关系
builder.HasOne<User>(p => p.User)
.WithOne(u => u.Profile)
.HasForeignKey<UserProfile>(p => p.UserId)
.IsRequired(); // 外键非空,强化一对一约束
}
}
2. 一对多关系
一对多是最常用的关系(如 “文章 - 评论”“订单 - 订单项”),建议在 “多” 的一方配置(更符合业务逻辑),也可根据需求配置单向 / 双向导航。
语法
// 基础语法(双向导航)
HasOne<主实体>(c => c.主实体导航属性) // 多的一方关联一个主实体
.WithMany(a => a.从实体集合属性); // 主实体关联多个从实体
// 单向导航(仅从“多”到“一”,主实体无集合导航)
HasOne<主实体>(c => c.主实体导航属性)
.WithMany(); // 无集合参数,表示单向
示例
实体类:
// 主实体:文章
public class Article
{
public long Id { get; set; }
public string Title { get; set; }
// 导航属性:一个文章有多个评论
public List<Comment> Comments { get; set; } = new List<Comment>();
}
// 从实体:评论(多的一方)
public class Comment
{
public long Id { get; set; }
public string Message { get; set; }
// 可选:显式声明外键(推荐,便于手动赋值和查询)
public long ArticleId { get; set; }
// 导航属性:一个评论属于一个文章
public Article Article { get; set; }
}
配置类:
public class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comments");
// 配置一对多关系
builder.HasOne<Article>(c => c.Article) // 评论关联一个文章
.WithMany(a => a.Comments) // 一个文章有多个评论
.HasForeignKey(c => c.ArticleId) // 显式指定外键(若实体声明了外键则建议加)
.IsRequired(); // 评论必须关联文章(外键非空)
// 字段约束
builder.Property(c => c.Message).IsRequired().IsUnicode();
}
}
关联数据查询
通过 Include 方法预加载关联数据(避免懒加载的性能问题):
// 读取ID=1的文章及其所有评论
using (var ctx = new MyDbContext())
{
Article a = ctx.Articles
.Include(a => a.Comments) // 预加载评论集合
.Single(a => a.Id == 1);
Console.WriteLine($"文章标题:{a.Title}");
foreach (Comment c in a.Comments)
{
Console.WriteLine($"评论ID:{c.Id},内容:{c.Message}");
}
}
注意:
- 若实体中未显式声明外键,EF Core 会自动生成隐式外键(如
ArticleId),但手动声明外键更易维护(便于手动赋值、查询过滤); - 除非业务不需要直接操作外键,否则建议显式声明(所谓 “重复” 问题在实际开发中几乎可忽略,反而能提升代码可读性)。
3. 多对多关系
多对多关系(如 “学生 - 课程”“商品 - 订单”)需要中间关联表,EF Core 5+ 支持无需显式定义中间实体,直接通过 UsingEntity 指定关联表名即可。
语法
HasMany<关联实体>(c => c.集合导航属性) // 当前实体关联多个关联实体
.WithMany(a => a.反向集合导航属性) // 关联实体关联多个当前实体
.UsingEntity(j => j.ToTable("关联表名")); // 指定中间关联表的表名
示例
实体类:
// 学生
public class Student
{
public long Id { get; set; }
public string Name { get; set; }
// 导航属性:一个学生选多门课程
public List<Course> Courses { get; set; } = new List<Course>();
}
// 课程
public class Course
{
public long Id { get; set; }
public string CourseName { get; set; }
// 导航属性:一门课程被多个学生选
public List<Student> Students { get; set; } = new List<Student>();
}
配置类:
public class StudentConfig : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("T_Students");
// 配置多对多关系
builder.HasMany<Course>(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("T_StudentCourse")); // 中间表名:T_StudentCourse
}
}
自定义中间表字段
若需要给中间表加额外字段(如选课时间),需显式定义中间实体:
// 中间实体:学生选课记录
public class StudentCourse
{
public long StudentId { get; set; }
public long CourseId { get; set; }
public DateTime SelectTime { get; set; } // 额外字段:选课时间
// 导航属性
public Student Student { get; set; }
public Course Course { get; set; }
}
// 配置多对多(关联中间实体)
builder.HasMany<Course>(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity<StudentCourse>(
j => j.HasOne<Course>(sc => sc.Course).WithMany().HasForeignKey(sc => sc.CourseId),
j => j.HasOne<Student>(sc => sc.Student).WithMany().HasForeignKey(sc => sc.StudentId),
j =>
{
j.ToTable("T_StudentCourse");
j.Property(sc => sc.SelectTime).IsRequired(); // 约束额外字段
});
二、自引用的组织结构树
自引用关系是特殊的一对多关系(实体自身关联自身),常用于实现 “部门树”“分类树”“组织架构树” 等层级结构。
1. 实体类定义
public class OrgUnit
{
public long Id { get; set; }
public string Name { get; set; } // 组织单元名称(如“技术部”“研发组”)
// 导航属性:父级组织(一对一)
public OrgUnit Parent { get; set; }
// 可选:显式声明父级外键(推荐,便于查询和赋值)
public long? ParentId { get; set; } // 可空,根节点无父级
// 导航属性:子级组织(一对多)
public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();
}
2. 关系配置
public class OrgUnitConfig : IEntityTypeConfiguration<OrgUnit>
{
public void Configure(EntityTypeBuilder<OrgUnit> builder)
{
builder.ToTable("T_OrgUnits");
// 配置自引用一对多关系
builder.HasOne<OrgUnit>(u => u.Parent) // 当前组织关联一个父组织
.WithMany(p => p.Children) // 一个父组织有多个子组织
.HasForeignKey(u => u.ParentId) // 指定外键(ParentId)
.OnDelete(DeleteBehavior.Restrict); // 级联删除限制(避免删除父级时误删所有子级)
}
}
3. 数据操作示例
添加层级数据
(根节点 + 子节点)
using (var ctx = new MyDbContext())
{
// 根节点:总公司
var root = new OrgUnit
{
Name = "总公司",
ParentId = null, // 根节点无父级
Children = new List<OrgUnit>
{
// 一级子节点:技术部
new OrgUnit { Name = "技术部", ParentId = 0 }, // 先临时赋值,Add后自动更新
// 一级子节点:市场部
new OrgUnit
{
Name = "市场部",
Children = new List<OrgUnit>
{
// 二级子节点:市场部-华东区
new OrgUnit { Name = "华东区" }
}
}
}
};
// 只需添加根节点,EF Core 会自动级联添加所有子节点
ctx.OrgUnits.Add(root);
ctx.SaveChanges();
}
查询层级数据
(加载所有子节点)
// 加载根节点及其所有子节点(递归预加载)
var rootOrg = ctx.OrgUnits
.Include(o => o.Children) // 加载一级子节点
.ThenInclude(c => c.Children) // 加载二级子节点(可多层嵌套)
.First(o => o.ParentId == null);
// 遍历输出组织架构
Console.WriteLine($"根节点:{rootOrg.Name}");
foreach (var firstChild in rootOrg.Children)
{
Console.WriteLine($"- {firstChild.Name}");
foreach (var secondChild in firstChild.Children)
{
Console.WriteLine($" - {secondChild.Name}");
}
}
注意:
- 根节点的
ParentId必须为null,否则会导致层级错乱; - 级联删除需谨慎:建议设置
OnDelete(DeleteBehavior.Restrict),避免删除父节点时自动删除所有子节点; - 若层级较深(如超过 3 层),建议使用递归查询或 EF Core 的
Include多层嵌套加载子节点。
总结
- 关系配置核心:
HasOne/HasMany声明当前实体的导航属性,WithOne/WithMany声明关联实体的导航属性,HasForeignKey指定外键(一对一必须显式声明外键,一对多 / 多对多可选); - 一对多是基础:建议在 “多” 的一方配置,通过
Include预加载关联数据,避免懒加载性能问题; - 自引用关系:本质是特殊的一对多,需注意外键可空(根节点)、级联删除限制,添加数据时只需加根节点即可级联保存子节点。
783



被折叠的 条评论
为什么被折叠?



