首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >EF Core 数据库关系配置全解:用最简单的特性搞懂一对一、一对多、多对多

EF Core 数据库关系配置全解:用最简单的特性搞懂一对一、一对多、多对多

原创
作者头像
步步为营DotNet
发布2026-07-09 11:49:30
发布2026-07-09 11:49:30
290
举报

很多人刚接触 EF Core 的时候,最容易写得出来的是:

  • 一个实体
  • 一个 DbSet
  • 一次 Add / SaveChanges

但一到真实项目,最核心的问题马上出现:

  • 用户和用户详情是一对一,怎么配?
  • 订单和订单项是一对多,怎么写最清晰?
  • 用户和角色是多对多,EF Core 到底要不要中间表实体?
  • 外键到底是用约定推断,还是手工标注?
  • 特性 [Key][ForeignKey][Required][NotMapped] 到底该什么时候用?
  • 为什么有些关系你“看起来写对了”,实际生成的表结构却很奇怪?

这篇文章不讲“Hello World”,我们专门聚焦 EF Core 中数据库最常用的关系配置,并且遵循一个原则:

优先使用最简单、最直观、最适合团队协作的特性标注方式(Data Annotation)来讲解。

也就是说,这篇文章重点不是上来就堆 Fluent API,而是先帮你建立对关系映射的正确认知:

  • 哪些关系是 EF Core 通过约定就能识别的
  • 哪些地方最好显式标注
  • 为什么某些特性必须加
  • 加了之后底层映射到底发生了什么

文章结构固定:

  • 痛点引入
  • 底层原理
  • 代码实战示例
  • 避坑要点
  • 性能 / 架构建议
  • 总结

一、痛点引入:为什么 EF Core 的关系配置总是“看起来会了,实际一做就乱”?

EF Core 在简单场景下非常友好。

比如你写一个 User

代码语言:csharp
复制
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}

然后 DbContext 里加一个:

代码语言:csharp
复制
public DbSet<User> Users { get; set; }

迁移、建库、增删改查都很顺。

但只要一进入真实业务模型,复杂度立刻上升。

比如:

  • 一个用户有一个详情资料
  • 一个用户有很多订单
  • 一个订单有很多订单项
  • 一个用户可以有多个角色,一个角色也能分配给多个用户
  • 一个博客有多个标签,一个标签也能归属于多个博客
  • 一个员工可能有一个直属领导,还是自引用关系

这时候很多项目会出现几类典型问题:

1. 实体写出来了,但关系并不符合预期

比如你以为写的是一对一,结果数据库里变成了一对多。

原因通常是:

  • 外键没有标对
  • 导航属性不完整
  • 主键 / 外键约束缺失
  • 唯一性没有体现

2. 表结构生成“意外字段”

例如迁移后发现数据库里多出:

  • UserId1
  • RoleId1
  • OrderId1

这通常意味着:

EF Core 没看懂你真正想表达的关系,于是根据约定又帮你“猜”了一套关系。


3. 多对多最容易误解

很多人以为 EF Core 的多对多就只要写两个集合导航属性就行。

实际上这取决于你的业务需求:

  • 如果只是简单关联,可以直接隐式多对多
  • 如果中间表还有额外字段,比如“分配时间”“创建人”“状态”,就必须显式中间实体

这是实战里非常容易踩坑的地方。


4. 特性标注和 Fluent API 混着写,最后谁生效都说不清

团队里经常会出现:

  • 实体上有 [ForeignKey]
  • OnModelCreating 里又重新配一遍
  • 最后连自己都不知道谁才是主配置

所以本文会先以 最简单的特性标注 为主线讲明白,帮助你建立“关系映射的本质认知”。


二、底层原理:EF Core 到底是怎么识别实体关系的?

要想把关系配明白,必须先理解 EF Core 是怎么“猜”关系的。

EF Core 配置关系主要依赖三种方式:

1. Convention(约定)

EF Core 会根据命名规则自动推断。

例如:

  • Id{EntityName}Id 会被识别为主键
  • UserId + User 导航属性,通常会被识别为外键关系
  • 集合导航属性通常会被识别为一对多 / 多对多

这也是为什么有些代码你没写任何特性,EF Core 也能工作。


2. Data Annotation(特性标注)

也就是本文重点。

常见特性包括:

  • [Key]
  • [Required]
  • [ForeignKey]
  • [NotMapped]
  • [MaxLength]
  • [Table]
  • [Column]

特性的好处是:

  • 配置贴近实体
  • 可读性直观
  • 简单场景非常高效
  • 团队协作时容易看懂

3. Fluent API(流式配置)

OnModelCreating 中写:

代码语言:csharp
复制
modelBuilder.Entity<Order>()
    .HasOne(o => o.User)
    .WithMany(u => u.Orders)
    .HasForeignKey(o => o.UserId);

它比特性更强大,适用于复杂映射。


2.1 实体关系的本质,其实就是“外键 + 导航属性 + 约束”

无论是一对一、一对多还是多对多,本质都离不开三个概念:

1)主键(Primary Key)

唯一标识一行数据。

2)外键(Foreign Key)

一张表通过某个字段引用另一张表的主键。

3)导航属性(Navigation Property)

让你在代码层面可以从一个实体访问关联实体。

比如:

代码语言:csharp
复制
public int UserId { get; set; }
public User User { get; set; }

其中:

  • UserId 是外键字段
  • User 是导航属性

关键理解

数据库真正维护关系的是 外键约束

导航属性只是 EF Core 为了对象编程体验提供的“代码入口”。

也就是说:

数据库关系的底层是真实字段约束,EF Core 关系的上层是导航属性。


2.2 为什么“一对一”最容易出错?

因为一对一不是光靠“两个实体互相引用”就能成立的。

例如:

代码语言:csharp
复制
public class User
{
    public int Id { get; set; }
    public UserProfile Profile { get; set; }
}

public class UserProfile
{
    public int Id { get; set; }
    public User User { get; set; }
}

你觉得这看起来像一对一。

但从数据库角度讲,它还缺少一个非常关键的信息:

谁持有外键?这个外键是不是唯一?

如果外键不唯一,那数据库层面其实仍然允许一对多。

所以一对一的关键,不是“互相有导航属性”,而是:

  • 必须明确外键在哪一方
  • 并且这个外键具备唯一性语义

在最常见的设计里,一对一通常有两种方式:

方式 1:共享主键

子表主键同时也是外键。

方式 2:独立外键 + 唯一索引

子表单独有外键列,但该列必须唯一。

对于“最简单特性标注”的讲法,我们通常更推荐理解 共享主键一对一,因为它语义最清晰。


三、代码实战示例:从实体定义到关系说明,逐个讲透

下面所有示例都基于:

  • .NET 8
  • EF Core 8
  • SQL Server
  • Data Annotation 特性优先

3.1 准备工作:安装包与 DbContext

先创建一个项目:

代码语言:bash
复制
dotnet new console -n EfCoreRelationDemo
cd EfCoreRelationDemo

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design

AppDbContext.cs

代码语言:csharp
复制
using Microsoft.EntityFrameworkCore;

namespace EfCoreRelationDemo;

public class AppDbContext : DbContext
{
    public DbSet<User> Users => Set<User>();
    public DbSet<UserProfile> UserProfiles => Set<UserProfile>();
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();
    public DbSet<Role> Roles => Set<Role>();
    public DbSet<UserRole> UserRoles => Set<UserRole>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            "Server=localhost;Database=EfCoreRelationDemo;Trusted_Connection=True;TrustServerCertificate=True");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 这里暂时只补充多对多主键配置
        modelBuilder.Entity<UserRole>()
            .HasKey(ur => new { ur.UserId, ur.RoleId });
    }
}

注意:因为复合主键无法仅靠 [Key] 特性优雅定义,所以多对多中间表这里会用到一点 Fluent API。 这不影响“主体讲解仍以最简单特性为主”。


3.2 一对一关系:User 和 UserProfile

业务场景

一个用户有一份详细资料:

  • 用户主表 User
  • 用户详情表 UserProfile

例如:

  • 用户名、手机号放主表
  • 地址、生日、头像等放详情表

实体定义

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCoreRelationDemo;

public class User
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MaxLength(50)]
    public string UserName { get; set; } = string.Empty;

    // 一对一导航属性
    public UserProfile? Profile { get; set; }
}

public class UserProfile
{
    // 共享主键:主键同时也是外键
    [Key]
    [ForeignKey(nameof(User))]
    public int Id { get; set; }

    [MaxLength(100)]
    public string? Address { get; set; }

    public DateTime? Birthday { get; set; }

    // 反向导航属性
    public User User { get; set; } = null!;
}

为什么这样标?

这里最关键的是这一句:

代码语言:csharp
复制
[ForeignKey(nameof(User))]
public int Id { get; set; }

意思是:

  • UserProfile.Id 本身是主键
  • 它同时也是指向 User 的外键

这就实现了 共享主键一对一


为什么推荐这种写法?

因为它天然表达了:

UserProfile 不能脱离 User 单独存在,而且一个 User 最多只有一个 UserProfile

数据库层面会更干净:

  • Users.Id
  • UserProfiles.Id(既是 PK,也是 FK)

不会额外多一个 UserId 字段。


示例插入代码

代码语言:csharp
复制
using EfCoreRelationDemo;

using var db = new AppDbContext();

db.Database.EnsureCreated();

var user = new User
{
    UserName = "alice",
    Profile = new UserProfile
    {
        Address = "上海市浦东新区",
        Birthday = new DateTime(1995, 5, 20)
    }
};

db.Users.Add(user);
db.SaveChanges();

Console.WriteLine("一对一数据保存成功");

EF Core 为什么能自动保存关联对象?

因为:

  • User 是聚合根式入口
  • Profile 是导航属性
  • EF Core 追踪了整棵对象图
  • SaveChanges() 时会按依赖顺序插入

先插 User,拿到 Id,再插 UserProfile


3.3 一对多关系:User 和 Order

业务场景

一个用户可以有多个订单,一个订单只属于一个用户。

这是一对多里最经典、最常见的场景。


实体定义

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCoreRelationDemo;

public class Order
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MaxLength(30)]
    public string OrderNo { get; set; } = string.Empty;

    public decimal TotalAmount { get; set; }

    // 外键字段
    [ForeignKey(nameof(User))]
    public int UserId { get; set; }

    // 多对一导航属性:多个订单属于一个用户
    public User User { get; set; } = null!;
}

同时扩展 User

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations;

namespace EfCoreRelationDemo;

public partial class User
{
    // 一对多导航属性:一个用户有多个订单
    public List<Order> Orders { get; set; } = new();
}

如果你不想用 partial,也可以直接把 Orders 写回原始 User 类里。 这里仅为演示拆分。


为什么这样标?

核心代码:

代码语言:csharp
复制
[ForeignKey(nameof(User))]
public int UserId { get; set; }

public User User { get; set; } = null!;

它表达的是:

  • Order.UserId 是外键
  • Order.User 是引用导航
  • User.Orders 是集合导航

这就是一对多最标准的写法。


为什么外键字段建议显式写出来?

很多初学者喜欢只写导航属性:

代码语言:csharp
复制
public User User { get; set; }

让 EF Core 自动生成影子外键(Shadow FK)。

虽然能工作,但不推荐在真实项目这么做,原因有三点:

1)可读性差

你在代码里看不到真实外键字段。

2)调试麻烦

查询、更新、日志输出时都不直观。

3)DTO / API 交互不方便

实际业务里经常直接传 UserId,而不是整个 User 对象。

所以建议:

一对多关系里,外键字段尽量显式声明。


示例插入代码

代码语言:csharp
复制
using EfCoreRelationDemo;

using var db = new AppDbContext();

var user = new User
{
    UserName = "bob",
    Orders = new List<Order>
    {
        new Order
        {
            OrderNo = "ORD2024001",
            TotalAmount = 199.00m
        },
        new Order
        {
            OrderNo = "ORD2024002",
            TotalAmount = 299.00m
        }
    }
};

db.Users.Add(user);
db.SaveChanges();

Console.WriteLine("一对多数据保存成功");

3.4 一对多关系:Order 和 OrderItem

业务场景

一个订单有多个订单项。

比如:

  • 订单号:ORD2024001
  • 商品 A 一条
  • 商品 B 两条

实体定义

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCoreRelationDemo;

public class OrderItem
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MaxLength(100)]
    public string ProductName { get; set; } = string.Empty;

    public int Quantity { get; set; }

    public decimal UnitPrice { get; set; }

    [ForeignKey(nameof(Order))]
    public int OrderId { get; set; }

    public Order Order { get; set; } = null!;
}

同时扩展 Order

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations;

namespace EfCoreRelationDemo;

public partial class Order
{
    public List<OrderItem> Items { get; set; } = new();
}

为什么这又是一对多,而不是多对多?

因为订单项本身是业务实体,不是简单关联。

它有自己的属性:

  • 商品名
  • 数量
  • 单价

也就是说,OrderItem 不是“订单和商品的纯关联表”,而是一个真实业务对象。

这点非常重要。

很多人看到“订单包含多个商品”,下意识会去设计多对多,但实际上:

只要中间对象有独立业务属性,它通常就应该被建模成独立实体,而不是隐式多对多。


示例插入代码

代码语言:csharp
复制
using EfCoreRelationDemo;

using var db = new AppDbContext();

var order = new Order
{
    OrderNo = "ORD2024003",
    TotalAmount = 398.00m,
    UserId = 1,
    Items = new List<OrderItem>
    {
        new OrderItem
        {
            ProductName = "机械键盘",
            Quantity = 1,
            UnitPrice = 199.00m
        },
        new OrderItem
        {
            ProductName = "游戏鼠标",
            Quantity = 1,
            UnitPrice = 199.00m
        }
    }
};

db.Orders.Add(order);
db.SaveChanges();

Console.WriteLine("订单与订单项保存成功");

3.5 多对多关系:User 和 Role

业务场景

一个用户可以拥有多个角色,一个角色也可以分配给多个用户。

例如:

  • Alice 拥有 Admin、Editor
  • Bob 拥有 User
  • Admin 角色可被多个用户共享

这就是标准多对多。


3.5.1 最推荐的实战方式:显式中间表实体

虽然 EF Core 支持隐式多对多,但真实项目里我更推荐:

显式定义中间实体。

原因很简单:

  • 将来很可能要加字段
  • 比如分配时间、创建人、状态、租户ID
  • 代码更明确
  • 更易扩展

实体定义

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations;

namespace EfCoreRelationDemo;

public class Role
{
    [Key]
    public int Id { get; set; }

    [Required]
    [MaxLength(50)]
    public string RoleName { get; set; } = string.Empty;

    public List<UserRole> UserRoles { get; set; } = new();
}

扩展 User

代码语言:csharp
复制
namespace EfCoreRelationDemo;

public partial class User
{
    public List<UserRole> UserRoles { get; set; } = new();
}

中间实体:

代码语言:csharp
复制
using System.ComponentModel.DataAnnotations.Schema;

namespace EfCoreRelationDemo;

public class UserRole
{
    [ForeignKey(nameof(User))]
    public int UserId { get; set; }

    public User User { get; set; } = null!;

    [ForeignKey(nameof(Role))]
    public int RoleId { get; set; }

    public Role Role { get; set; } = null!;
}

为什么这里没有用 [Key]

因为这里主键通常是复合主键:

代码语言:csharp
复制
UserId + RoleId

[Key] 特性更适合单字段主键。

EF Core 当前对复合主键更推荐在 OnModelCreating 里配:

代码语言:csharp
复制
modelBuilder.Entity<UserRole>()
    .HasKey(ur => new { ur.UserId, ur.RoleId });

为什么这依然是“最简单实战写法”?

因为中间实体上的关系本身已经通过最简单的特性标明了:

  • [ForeignKey(nameof(User))]
  • [ForeignKey(nameof(Role))]

只是 复合主键这个点 用特性不够优雅,所以借助一行 Fluent API。

这在工程实践里是完全合理的。


示例插入代码

代码语言:csharp
复制
using EfCoreRelationDemo;

using var db = new AppDbContext();

var adminRole = new Role { RoleName = "Admin" };
var editorRole = new Role { RoleName = "Editor" };

var user = new User
{
    UserName = "charlie",
    UserRoles = new List<UserRole>
    {
        new UserRole { Role = adminRole },
        new UserRole { Role = editorRole }
    }
};

db.Users.Add(user);
db.SaveChanges();

Console.WriteLine("多对多数据保存成功");

3.5.2 为什么不推荐初学者一上来就用隐式多对多?

EF Core 支持直接这样写:

代码语言:csharp
复制
public class User
{
    public List<Role> Roles { get; set; } = new();
}

public class Role
{
    public List<User> Users { get; set; } = new();
}

EF Core 会自动生成中间表。

这当然很方便,但问题是:

1)一旦中间表要加字段,就要重构

比如你后面想记录:

  • AssignedAt
  • AssignedBy
  • Status

这时就必须改成显式中间实体。

2)表结构和业务语义不够直观

很多团队协作时,看不到中间实体,不利于统一理解。

3)复杂场景不好扩展

比如权限系统、多租户、软删除等,中间表经常需要更多控制。

所以我的建议是:

学习阶段可以了解隐式多对多,但真实业务建议优先显式中间实体。


四、常用特性详细解释:它们到底解决了什么问题?

下面把本文最常出现的特性做一次系统说明。


4.1 [Key]

作用

指定主键。

示例

代码语言:csharp
复制
[Key]
public int Id { get; set; }

为什么要用?

如果你的命名就是 Id{EntityName}Id,其实 EF Core 通常能自动识别。

但显式加 [Key] 有两个好处:

  • 代码意图更明确
  • 对初学者和团队成员更友好

什么时候推荐加?

  • 演示代码、教学代码建议加
  • 主键不走默认命名时必须加

4.2 [Required]

作用

表示不能为空。

示例

代码语言:csharp
复制
[Required]
public string UserName { get; set; } = string.Empty;

为什么要用?

它同时表达两层意思:

  • 代码层面:这是必填字段
  • 数据库层面:生成 NOT NULL

注意

对于 C# 可空引用类型开启后,像:

代码语言:csharp
复制
public string UserName { get; set; } = string.Empty;

很多时候 EF Core 已经能推断为必填。

[Required] 依然有文档化价值。


4.3 [ForeignKey]

作用

明确指定哪个字段是外键,或某导航属性对应的外键。

示例

代码语言:csharp
复制
[ForeignKey(nameof(User))]
public int UserId { get; set; }

为什么要用?

因为它能显式告诉 EF Core:

这个字段对应的是 User 导航属性的外键

这有助于避免 EF Core 因命名不规范而推断错误,防止生成多余字段。


4.4 [MaxLength]

作用

限制字符串最大长度。

示例

代码语言:csharp
复制
[MaxLength(50)]
public string UserName { get; set; } = string.Empty;

为什么要用?

原因非常实际:

  • 防止数据库生成无限长类型
  • 更符合业务约束
  • 提高索引可控性
  • 避免脏数据

4.5 [NotMapped]

作用

表示该属性不映射到数据库。

示例

代码语言:csharp
复制
[NotMapped]
public string DisplayName => $"{FirstName} {LastName}";

为什么要用?

有些属性只是计算属性、视图字段,不需要入库。


五、避坑要点:这些问题几乎每个 EF Core 项目都会遇到


5.1 不要只写导航属性,不写外键字段

错误倾向:

代码语言:csharp
复制
public User User { get; set; }

没有 UserId

虽然 EF Core 可以用影子外键处理,但不推荐。

为什么?

  • 不利于调试
  • 不利于 API 传参
  • SQL 排查时不直观
  • 容易让团队成员迷惑

推荐始终显式声明外键字段。


5.2 一对一不是“互相导航”就够了

很多人误以为:

代码语言:csharp
复制
public UserProfile Profile { get; set; }
public User User { get; set; }

就是一对一。

其实不是。

必须保证数据库层面具备“一对一约束”,最简单方式就是共享主键。


5.3 多对多中间表未来大概率会加字段

所以别因为图省事就上来用隐式多对多。

尤其是这些场景:

  • 用户角色分配
  • 用户组织归属
  • 用户标签
  • 商品分类
  • 博客标签

中间表往往很快就会扩展字段。


5.4 特性标注和 Fluent API 不要重复打架

建议团队统一规范:

简单关系

优先 Data Annotation

复杂关系 / 复合主键 / 索引 / 删除行为

用 Fluent API

不要同一个关系在两个地方都写,容易造成困惑。


5.5 别把业务聚合关系和数据库关系混为一谈

例如:

  • 订单和订单项是一对多
  • 订单和商品未必是多对多
  • 因为订单项本身就是业务实体

建模时要先看:

这个“中间对象”是不是有自己的业务意义?

有的话,它就不是“纯关系表”。


六、性能 / 架构建议:关系配对了,只是第一步


6.1 实体关系越复杂,越要控制查询方式

很多新手在 EF Core 里写关系后,最喜欢这样查:

代码语言:csharp
复制
var users = db.Users
    .Include(u => u.Orders)
    .ThenInclude(o => o.Items)
    .ToList();

这在数据量小时没问题,但一旦层级深、数据大,就会有:

  • 笛卡尔积放大
  • 重复数据传输
  • 内存占用大
  • SQL 极其复杂

建议

列表页优先投影查询:

代码语言:csharp
复制
var result = db.Users
    .Select(u => new
    {
        u.Id,
        u.UserName,
        OrderCount = u.Orders.Count
    })
    .ToList();

不要无脑 Include 全家桶。


6.2 关系实体要符合业务边界,而不是只看数据库范式

比如:

  • OrderItem 应该是实体
  • UserRole 在权限系统中也最好是实体
  • 因为它们通常会承载业务属性

设计时优先考虑业务语义,再考虑 EF Core 怎么映射。


6.3 特性适合简单关系,复杂控制还是 Fluent API 更稳

你要求的是“用最简单的特性来标注”,这完全合理,也最适合学习和多数业务场景。

但要明确边界:

特性适合:

  • 主键
  • 必填
  • 长度
  • 简单外键
  • 简单一对一 / 一对多

Fluent API 更适合:

  • 复合主键
  • 唯一索引
  • 删除行为
  • 精细列配置
  • 复杂继承映射
  • 高级约束

最好的工程实践不是“只用一种”,而是:

简单的用特性,复杂的交给 Fluent API。


七、总结

EF Core 的关系配置,表面上看是在写实体类,实际上你做的是两层映射:

  • 数据库层:主键、外键、约束
  • 对象层:导航属性、集合关系、对象图追踪

本文重点用最简单、最常用的特性讲了三种核心关系:

一对一

推荐共享主键方式,语义最清晰:

  • 子表主键就是外键
  • 一眼就能看出“只能对应一个父实体”

一对多

最常见,也最实用:

  • 子表显式外键字段
  • 子表引用导航
  • 父表集合导航

多对多

真实项目优先显式中间实体:

  • 方便扩展
  • 业务更清晰
  • 后期更稳

同时你也应该记住一个核心原则:

导航属性只是面向对象的便利,真正定义关系的是数据库中的主键与外键约束。

如果你把这个底层逻辑理解透了,EF Core 的各种关系配置就不会再只是“背语法”,而是真正理解为什么这么设计。


原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、痛点引入:为什么 EF Core 的关系配置总是“看起来会了,实际一做就乱”?
    • 1. 实体写出来了,但关系并不符合预期
    • 2. 表结构生成“意外字段”
    • 3. 多对多最容易误解
    • 4. 特性标注和 Fluent API 混着写,最后谁生效都说不清
  • 二、底层原理:EF Core 到底是怎么识别实体关系的?
    • 1. Convention(约定)
    • 2. Data Annotation(特性标注)
    • 3. Fluent API(流式配置)
    • 2.1 实体关系的本质,其实就是“外键 + 导航属性 + 约束”
      • 1)主键(Primary Key)
      • 2)外键(Foreign Key)
      • 3)导航属性(Navigation Property)
      • 关键理解
    • 2.2 为什么“一对一”最容易出错?
      • 方式 1:共享主键
      • 方式 2:独立外键 + 唯一索引
  • 三、代码实战示例:从实体定义到关系说明,逐个讲透
    • 3.1 准备工作:安装包与 DbContext
      • AppDbContext.cs
    • 3.2 一对一关系:User 和 UserProfile
      • 业务场景
      • 实体定义
      • 为什么这样标?
      • 为什么推荐这种写法?
      • 示例插入代码
      • EF Core 为什么能自动保存关联对象?
    • 3.3 一对多关系:User 和 Order
      • 业务场景
      • 实体定义
      • 为什么这样标?
      • 为什么外键字段建议显式写出来?
      • 示例插入代码
    • 3.4 一对多关系:Order 和 OrderItem
      • 业务场景
      • 实体定义
      • 为什么这又是一对多,而不是多对多?
      • 示例插入代码
    • 3.5 多对多关系:User 和 Role
      • 业务场景
    • 3.5.1 最推荐的实战方式:显式中间表实体
      • 实体定义
      • 为什么这里没有用 [Key]?
      • 为什么这依然是“最简单实战写法”?
      • 示例插入代码
    • 3.5.2 为什么不推荐初学者一上来就用隐式多对多?
      • 1)一旦中间表要加字段,就要重构
      • 2)表结构和业务语义不够直观
      • 3)复杂场景不好扩展
  • 四、常用特性详细解释:它们到底解决了什么问题?
    • 4.1 [Key]
      • 作用
      • 示例
      • 为什么要用?
      • 什么时候推荐加?
    • 4.2 [Required]
      • 作用
      • 示例
      • 为什么要用?
      • 注意
    • 4.3 [ForeignKey]
      • 作用
      • 示例
      • 为什么要用?
    • 4.4 [MaxLength]
      • 作用
      • 示例
      • 为什么要用?
    • 4.5 [NotMapped]
      • 作用
      • 示例
      • 为什么要用?
  • 五、避坑要点:这些问题几乎每个 EF Core 项目都会遇到
    • 5.1 不要只写导航属性,不写外键字段
      • 为什么?
    • 5.2 一对一不是“互相导航”就够了
    • 5.3 多对多中间表未来大概率会加字段
    • 5.4 特性标注和 Fluent API 不要重复打架
      • 简单关系
      • 复杂关系 / 复合主键 / 索引 / 删除行为
    • 5.5 别把业务聚合关系和数据库关系混为一谈
  • 六、性能 / 架构建议:关系配对了,只是第一步
    • 6.1 实体关系越复杂,越要控制查询方式
      • 建议
    • 6.2 关系实体要符合业务边界,而不是只看数据库范式
    • 6.3 特性适合简单关系,复杂控制还是 Fluent API 更稳
      • 特性适合:
      • Fluent API 更适合:
  • 七、总结
    • 一对一
    • 一对多
    • 多对多
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档