C# Academy · 课时

关系:一对多与多对多

使用导航属性和 Fluent API 对一对多及多对多关系进行建模和查询。

第 4 / 4 课13 个步骤

关系:一对多与多对多 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. DbContext 与 DbSet 基础
  2. 迁移与架构管理
  3. 使用 EF Core 执行 CRUD 操作
  4. 关系:一对多与多对多
← 返回 C# Academy