📚 EF Core导航属性:为什么它不会成为数据库字段
🔍 问题起源
作为Java开发者转.NET,我们习惯了JPA/Hibernate中的关联映射。在Java中,我们经常这样写:
java
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems; // 这个不会成为数据库字段吗?
}
在.NET ABP项目中,我们也看到了类似的写法,但是我在新微服务项目中用的是DDD开发思想,但是用ORM刷数据库表的时候会不会把关联熟悉刷进去:
csharp
public class Commission : FullAuditedAggregateRoot<long>
{
// 这个会变成数据库字段吗?
public virtual ICollection<CommissionAttachment> Attachments { get; set; }
}
🎯 核心结论
导航属性(如 ICollection<T>)不会直接映射为数据库表的字段!
🏗️ 数据库 vs 代码:两种不同的存在形式
1. 数据库层面:表和关系
sql
-- Commissions表(实际存储)
CREATE TABLE Commissions (
Id BIGINT PRIMARY KEY,
ContractNumber NVARCHAR(100), -- 实际字段
ContractAmt DECIMAL(18,2), -- 实际字段
... -- 其他实际字段
-- 没有 Attachments 字段!
);
-- CommissionAttachments表(实际存储)
CREATE TABLE CommissionAttachments (
Id BIGINT PRIMARY KEY,
CommissionId BIGINT, -- 外键(实际字段)
FileName NVARCHAR(255), -- 实际字段
-- 外键约束建立关联
FOREIGN KEY (CommissionId) REFERENCES Commissions(Id)
);
2. 代码层面:对象和导航
csharp
// 实体类 - 业务对象表示
public class Commission
{
// 这些会映射到数据库字段
public long Id { get; set; }
public string ContractNumber { get; set; }
public decimal? ContractAmt { get; set; }
// 这个不会映射到数据库字段!
// 它只是代码层面的对象引用
public virtual ICollection<CommissionAttachment> Attachments { get; set; }
}
🔄 EF Core的"魔法":如何建立关联
方式1:通过外键属性(最清晰)
csharp
public class CommissionAttachment
{
public long Id { get; set; }
public string FileName { get; set; }
// 这个才是数据库字段!
public long CommissionId { get; set; } // 外键列
// 导航属性(用于代码中访问关联对象)
public virtual Commission Commission { get; set; }
}
方式2:通过配置(在DbContext中)
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Commission>(entity =>
{
// 告诉EF Core:Commission有多个Attachment
entity.HasMany(c => c.Attachments)
// 每个Attachment属于一个Commission
.WithOne(a => a.Commission)
// 外键是CommissionAttachment表的CommissionId字段
.HasForeignKey(a => a.CommissionId);
});
}
📊 数据库迁移的实际生成
当你运行 dotnet ef migrations add 时,生成的是:
sql
-- 创建主表(没有导航属性)
CREATE TABLE [Commissions] (
[Id] bigint NOT NULL IDENTITY,
[ContractNumber] nvarchar(100) NULL,
[ContractAmt] decimal(18,2) NULL,
...其他实际字段,
CONSTRAINT [PK_Commissions] PRIMARY KEY ([Id])
);
-- 创建关联表(包含外键)
CREATE TABLE [CommissionAttachments] (
[Id] bigint NOT NULL IDENTITY,
[FileName] nvarchar(255) NULL,
[CommissionId] bigint NOT NULL, -- ← 这才是关键!
CONSTRAINT [PK_CommissionAttachments] PRIMARY KEY ([Id]),
-- 外键约束建立关系
CONSTRAINT [FK_CommissionAttachments_Commissions_CommissionId]
FOREIGN KEY ([CommissionId]) REFERENCES [Commissions] ([Id])
ON DELETE CASCADE
);
-- 创建索引提高查询性能
CREATE INDEX [IX_CommissionAttachments_CommissionId]
ON [CommissionAttachments] ([CommissionId]);
💡 理解这个设计的好处
1. 数据库规范化
- 避免数据冗余
- 保持表结构简洁
- 符合关系型数据库设计原则
2. 对象关系映射的真正意义
csharp
// 代码中:直观的对象访问
var commission = GetCommission(123);
foreach (var attachment in commission.Attachments)
{
Console.WriteLine(attachment.FileName);
}
// 底层实际执行(大致相当于):
// 1. SELECT * FROM Commissions WHERE Id = 123
// 2. SELECT * FROM CommissionAttachments WHERE CommissionId = 123
3. 灵活性
- 可以延迟加载(Lazy Loading)
- 可以预先加载(Eager Loading)
- 可以显式加载(Explicit Loading)
🧪 验证实验:创建简单的测试
步骤1:创建两个相关实体
csharp
public class Parent
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性
public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public string Name { get; set; }
// 外键
public int ParentId { get; set; }
// 导航属性
public virtual Parent Parent { get; set; }
}
步骤2:运行迁移
bash
dotnet ef migrations add CreateParentChild
dotnet ef database update
步骤3:查看生成的SQL
你会看到:
Parents表没有Children字段Children表有ParentId字段作为外键- 外键约束建立了表间关系
🎯 实际开发中的正确理解
作为开发者,你应该这样想:
-
数据库表存储数据
- 存储实际的数据值
- 通过外键建立表间关系
- 符合SQL和关系代数
-
代码实体表示业务对象
- 包含数据字段(映射到表列)
- 包含导航属性(表示对象关系)
- 提供业务逻辑和方法
-
EF Core负责桥梁作用
- 将对象操作转换为SQL
- 将查询结果组装为对象图
- 管理关联数据的加载策略
📝 总结要点
- 导航属性是纯代码概念,用于在内存中建立对象间关联
- 数据库通过外键字段和约束建立表间关系
- EF Core自动管理代码对象和数据库表之间的映射
- 迁移只生成实际的表和列,不会生成"虚拟"的导航属性字段
💭 思考题
如果你有一个 Order 实体,需要关联100个 OrderItem,数据库表中:
- ✅
Order表不会有一个包含100个item的字段 - ✅ 会有100行记录在
OrderItems表中 - ✅ 每行
OrderItems都有一个OrderId指向父订单 - ✅ 代码中通过
order.Items可以方便地访问这100个item
这就是ORM的威力:让代码更符合面向对象思维,让数据库保持关系型结构的优势。
记住:导航属性是你的代码助手,外键是数据库的关联机制。两者配合,各司其职。