关系:一对多与多对多
使用导航属性和 Fluent API 对一对多及多对多关系进行建模和查询。
关系:一对多与多对多 是 CoddyKit 上的免费 C# Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
EF Core 中的关系
EF Core 为实体之间的关系建模,支持三种类型:一对一、一对多和多对多。每种关系都通过导航属性表示,也可以选择使用流式 API 进行配置。
一对多:实体设置
一个 Blog 有多个 Post。从属实体(Post)持有外键。两端的导航属性允许双向遍历。
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public int BlogId { get; set; } // FK
public Blog Blog { get; set; } = null!; // nav property
}流式 API:一对多
EF Core 会根据约定推断关系,但您也可以使用流式 API 明确配置关系,以提高可读性并设置级联删除行为。
protected override void OnModelCreating(ModelBuilder mb)
{
mb.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
}使用 Include 查询(预先加载)
使用 Include 在同一个查询中加载相关数据。使用 ThenInclude 可以深入导航图的更深层级。
// Load blog with all its posts
var blog = await _db.Blogs
.Include(b => b.Posts)
.FirstOrDefaultAsync(b => b.Id == 1);
// Load posts with their comments too
var blogs = await _db.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();无连接实体的多对多关系
EF Core 5+ 支持不使用显式连接实体的多对多关系。它会根据实体类型名称自动创建连接表。
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Course> Courses { get; set; } = new List<Course>();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; } = "";
public ICollection<Student> Students { get; set; } = new List<Student>();
}
// EF creates StudentCourse join table automatically使用连接实体的多对多关系
当连接表需要额外的列(例如注册日期、成绩)时,请创建一个带有复合主键的显式连接实体。
public class Enrollment
{
public int StudentId { get; set; }
public Student Student { get; set; } = null!;
public int CourseId { get; set; }
public Course Course { get; set; } = null!;
public DateTime EnrolledAt { get; set; }
public decimal? Grade { get; set; }
}
// Fluent API config:
mb.Entity<Enrollment>().HasKey(e => new { e.StudentId, e.CourseId });
mb.Entity<Enrollment>().HasOne(e => e.Student).WithMany(s => s.Enrollments);
mb.Entity<Enrollment>().HasOne(e => e.Course).WithMany(c => c.Enrollments);插入相关数据
在调用 SaveChangesAsync 之前,将相关实体添加到父实体的集合中。EF Core 会自动处理 FK 值。
var blog = new Blog { Name = "Dev Notes" };
blog.Posts.Add(new Post { Title = "Hello EF Core" });
blog.Posts.Add(new Post { Title = "Migrations Deep Dive" });
_db.Blogs.Add(blog);
await _db.SaveChangesAsync();
// Inserts Blog + 2 Posts with correct BlogId FK按相关数据筛选
您可以在 LINQ 中根据导航属性进行筛选。EF Core 会自动将其转换为 SQL JOIN。
// Blogs that have at least one published post
var popularBlogs = await _db.Blogs
.Where(b => b.Posts.Any(p => p.IsPublished))
.Select(b => new { b.Name, PostCount = b.Posts.Count(p => p.IsPublished) })
.OrderByDescending(b => b.PostCount)
.ToListAsync();级联删除
使用 DeleteBehavior.Cascade 时,删除主体(Blog)会自动删除所有从属实体(帖子)。其他选项包括:SetNull、Restrict、NoAction。
// Delete a blog and all its posts
var blog = await _db.Blogs.FindAsync(1);
_db.Blogs.Remove(blog!);
await _db.SaveChangesAsync();
// Cascade: all Posts with BlogId=1 are also deleted用于值对象的拥有实体
拥有实体类型非常适合表示值对象(例如 Address)。它们与所有者存储在同一张表中,并且没有独立的标识。
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public Address ShippingAddress { get; set; } = new();
}
public class Address
{
public string Street { get; set; } = "";
public string City { get; set; } = "";
public string Country { get; set; } = "";
}
// Fluent API:
mb.Entity<Customer>().OwnsOne(c => c.ShippingAddress);实战:包含行项目的订单
一个完整的电子商务模式:订单(主体)包含多个 OrderLines(从属实体),每个 OrderLines 都引用一个 Product。
var order = await _db.Orders
.Include(o => o.Lines)
.ThenInclude(l => l.Product)
.FirstOrDefaultAsync(o => o.Id == orderId);
if (order is null) return Results.NotFound();
var summary = new
{
OrderId = order.Id,
Total = order.Lines.Sum(l => l.Quantity * l.Product.Price),
Items = order.Lines.Select(l => new
{
l.Product.Name,
l.Quantity,
LineTotal = l.Quantity * l.Product.Price
})
};
return Results.Ok(summary);快速检查
在什么情况下,EF Core 会自动创建连接表,而不需要显式连接实体?
回顾:EF Core 中的关系
核心要点:
- 一对多:FK 位于从属实体上;两端都有导航属性;使用 Include 加载
- 多对多(简单):两个 ICollection 导航属性——EF Core 会自动创建连接表
- 多对多(带附加数据):使用带有复合 PK 的显式连接实体
- 级联删除控制移除主体时从属实体的处理方式
- 拥有实体在与所有者相同的表中表示值对象
用 AI 导师学习 C# — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 93
- 课程
- 346
常见问题解答
「关系:一对多与多对多」课时是免费的吗?
是的 — 「关系:一对多与多对多」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「关系:一对多与多对多」这节课中我会学到什么?
使用导航属性和 Fluent API 对一对多及多对多关系进行建模和查询。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「关系:一对多与多对多」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。