Appearance
Owned Entities 值对象
目录
什么是 Owned Entities
概念理解
Owned Entities(拥有实体) 是 EF Core 2.0+ 引入的功能,用于映射值对象(Value Objects),这些对象没有自己的身份标识,完全依赖于所有者实体存在。
csharp
// 传统方式: 地址作为单独实体(有ID)
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
// ❌ 不推荐: 地址有自己的 ID,不符合值对象语义
public Address ShippingAddress { get; set; }
}
public class Address
{
public int Id { get; set; } // 💥 值对象不应该有独立 ID
public string Street { get; set; }
public string City { get; set; }
}
// ✅ 推荐: 使用 Owned Entity(无ID)
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
// 地址是值对象,没有独立身份
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
}
[Owned] // 标记为拥有实体
public class Address
{
// 没有 Id 属性!
public string Street { get; set; }
public string City { get; set; }
public string Province { get; set; }
public string ZipCode { get; set; }
}核心特征
✅ 无身份标识: 没有主键,不能独立存在
✅ 依赖所有者: 随所有者创建/删除
✅ 值语义: 通过属性值比较相等性
✅ 可复用: 同一类型可作为多个所有者的属性
✅ 表结构灵活: 可存储在所有者表中或单独表中
与值对象对比
| 特性 | DDD 值对象 | EF Core Owned Entity |
|---|---|---|
| 概念 | 领域驱动设计概念 | ORM 映射技术 |
| 实现 | 不可变类 | 可变类(通常) |
| 持久化 | 需要手动处理 | EF Core 自动处理 |
| 相等性 | 基于值比较 | 默认引用比较 |
| 用途 | 业务逻辑层 | 数据访问层 |
基本配置方法
方法 1: [Owned] 特性
csharp
using Microsoft.EntityFrameworkCore;
[Owned]
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string Province { get; set; }
public string ZipCode { get; set; }
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address ShippingAddress { get; set; }
public Address BillingAddress { get; set; }
}
// EF Core 自动识别 [Owned] 特性
// 无需额外配置!方法 2: Fluent API
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 方式 1: OwnsOne 单个值对象
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.ShippingAddress);
// 方式 2: OwnsOne 并自定义列名
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.ShippingAddress, a =>
{
a.Property(addr => addr.Street).HasColumnName("ShippingStreet");
a.Property(addr => addr.City).HasColumnName("ShippingCity");
a.Property(addr => addr.Province).HasColumnName("ShippingProvince");
a.Property(addr => addr.ZipCode).HasColumnName("ShippingZipCode");
});
// 方式 3: 配置第二个值对象
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.BillingAddress, a =>
{
a.Property(addr => addr.Street).HasColumnName("BillingStreet");
a.Property(addr => addr.City).HasColumnName("BillingCity");
a.Property(addr => addr.Province).HasColumnName("BillingProvince");
a.Property(addr => addr.ZipCode).HasColumnName("BillingZipCode");
});
}数据库结构
默认行为: 存储在所有者表中
sql
-- Customers 表(包含地址字段)
CREATE TABLE Customers (
Id INT PRIMARY KEY IDENTITY(1,1),
Name NVARCHAR(100) NOT NULL,
-- ShippingAddress 的字段(自动添加前缀)
ShippingAddress_Street NVARCHAR(200),
ShippingAddress_City NVARCHAR(100),
ShippingAddress_Province NVARCHAR(100),
ShippingAddress_ZipCode NVARCHAR(20),
-- BillingAddress 的字段
BillingAddress_Street NVARCHAR(200),
BillingAddress_City NVARCHAR(100),
BillingAddress_Province NVARCHAR(100),
BillingAddress_ZipCode NVARCHAR(20)
);
-- 示例数据:
-- | Id | Name | ShippingAddress_Street | ShippingAddress_City | ... |
-- |----|-------|------------------------|----------------------|-----|
-- | 1 | Alice | 长安街1号 | 北京 | ... |可选: 存储到单独表
csharp
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.ShippingAddress, a =>
{
a.ToTable("CustomerShippingAddresses"); // 单独表
a.Property(addr => addr.Street).HasColumnName("Street");
a.Property(addr => addr.City).HasColumnName("City");
// ...
});
// SQL:
-- CREATE TABLE CustomerShippingAddresses (
-- CustomerId INT PRIMARY KEY,
-- Street NVARCHAR(200),
-- City NVARCHAR(100),
-- ...
-- CONSTRAINT FK_CustomerShippingAddresses_Customers
-- FOREIGN KEY (CustomerId) REFERENCES Customers(Id) ON DELETE CASCADE
-- );集合与嵌套
Owned Entity 集合
csharp
[Owned]
public class OrderItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
public class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
// 订单项集合(值对象)
public List<OrderItem> Items { get; set; } = new();
}
// 配置
modelBuilder.Entity<Order>()
.OwnsMany(o => o.Items, oi =>
{
oi.WithOwner().HasForeignKey("OrderId"); // 外键
oi.Property<int>("Id"); // 隐藏的主键
oi.HasKey("Id");
oi.Property(item => item.ProductId);
oi.Property(item => item.ProductName);
oi.Property(item => item.Quantity);
oi.Property(item => item.UnitPrice);
});
// 数据库结构:
-- CREATE TABLE OrderItems (
-- Id INT PRIMARY KEY IDENTITY(1,1),
-- OrderId INT NOT NULL,
-- ProductId INT NOT NULL,
-- ProductName NVARCHAR(200),
-- Quantity INT NOT NULL,
-- UnitPrice DECIMAL(18,2) NOT NULL,
-- CONSTRAINT FK_OrderItems_Orders
-- FOREIGN KEY (OrderId) REFERENCES Orders(Id) ON DELETE CASCADE
-- );嵌套 Owned Entities
csharp
[Owned]
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
}
[Owned]
public class PriceInfo
{
public Money BasePrice { get; set; }
public Money Discount { get; set; }
public Money FinalPrice => new Money
{
Amount = BasePrice.Amount - Discount.Amount,
Currency = BasePrice.Currency
};
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
// 嵌套值对象
public PriceInfo Pricing { get; set; }
}
// 配置
modelBuilder.Entity<Product>()
.OwnsOne(p => p.Pricing, pi =>
{
pi.OwnsOne(p => p.BasePrice, bp =>
{
bp.Property(m => m.Amount).HasColumnName("BasePrice_Amount");
bp.Property(m => m.Currency).HasColumnName("BasePrice_Currency");
});
pi.OwnsOne(p => p.Discount, d =>
{
d.Property(m => m.Amount).HasColumnName("Discount_Amount");
d.Property(m => m.Currency).HasColumnName("Discount_Currency");
});
});
// 数据库结构:
-- CREATE TABLE Products (
-- Id INT PRIMARY KEY,
-- Name NVARCHAR(200),
-- BasePrice_Amount DECIMAL(18,2),
-- BasePrice_Currency NVARCHAR(10),
-- Discount_Amount DECIMAL(18,2),
-- Discount_Currency NVARCHAR(10)
-- );查询与更新
查询 Owned Entities
csharp
// 方式 1: Include 加载
var customer = await context.Customers
.Include(c => c.ShippingAddress)
.FirstOrDefaultAsync(c => c.Id == 1);
Console.WriteLine(customer.ShippingAddress.City);
// 方式 2: 投影查询(推荐)
var customerData = await context.Customers
.Where(c => c.Id == 1)
.Select(c => new
{
c.Name,
ShippingCity = c.ShippingAddress.City,
ShippingStreet = c.ShippingAddress.Street
})
.FirstOrDefaultAsync();
// 方式 3: 过滤条件
var customersInBeijing = await context.Customers
.Where(c => c.ShippingAddress.City == "北京")
.ToListAsync();
// SQL:
-- SELECT * FROM Customers WHERE ShippingAddress_City = N'北京'更新 Owned Entities
csharp
// 方式 1: 替换整个值对象
var customer = await context.Customers.FindAsync(1);
customer.ShippingAddress = new Address
{
Street = "新的街道",
City = "上海",
Province = "上海",
ZipCode = "200000"
};
await context.SaveChangesAsync();
// 方式 2: 修改属性
var customer = await context.Customers.FindAsync(1);
customer.ShippingAddress.City = "深圳";
customer.ShippingAddress.Street = "深南大道100号";
await context.SaveChangesAsync();
// 方式 3: 批量更新(EF Core 7+)
await context.Customers
.Where(c => c.ShippingAddress.City == "北京")
.ExecuteUpdateAsync(setters => setters
.SetProperty(c => c.ShippingAddress.City, "北京市"));查询 Owned Entity 集合
csharp
// 查询订单及其商品项
var order = await context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == 1);
foreach (var item in order.Items)
{
Console.WriteLine($"{item.ProductName} x {item.Quantity} = {item.Total}");
}
// 过滤集合
var ordersWithExpensiveItems = await context.Orders
.Where(o => o.Items.Any(i => i.UnitPrice > 100))
.ToListAsync();
// 统计
var orderSummary = await context.Orders
.Where(o => o.Id == 1)
.Select(o => new
{
o.OrderDate,
ItemCount = o.Items.Count,
TotalAmount = o.Items.Sum(i => i.Total)
})
.FirstOrDefaultAsync();.NET 8/9/10 新特性
.NET 8: 改进的 Owned Entity 性能
csharp
// .NET 8 优化了 Owned Entity 的变更跟踪
// 对于复杂嵌套值对象,性能提升 30-40%
var customer = await context.Customers.FindAsync(1);
customer.ShippingAddress.City = "New City";
await context.SaveChangesAsync(); // 更快的变更检测.NET 9: 增强的集合支持
csharp
// .NET 9 改进了 OwnsMany 的查询生成
// 对于大型集合,查询速度提升 25-35%
var orders = await context.Orders
.Include(o => o.Items)
.ToListAsync();
// .NET 9 生成更优化的 SQL,减少 JOIN 开销.NET 10: 智能值对象(路线图)
预计特性:
- 自动检测不可变值对象
- 基于值比较的相等性检查
- 运行时动态 Owned Entity 配置
- 值对象变更跟踪优化
最佳实践与陷阱
最佳实践
1. 选择合适的场景
使用 Owned Entity 如果:
✓ 对象没有独立身份(无ID)
✓ 对象依赖于所有者存在
✓ 对象可能被多个实体复用
✓ 对象表示值而非实体
不使用 Owned Entity 如果:
✗ 对象需要独立查询
✗ 对象需要在多个所有者间共享
✗ 对象有自己的生命周期
✗ 对象需要被其他实体引用2. 合理组织列名
csharp
// ✅ 推荐: 自定义列名,避免过长
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.ShippingAddress, a =>
{
a.Property(addr => addr.Street).HasColumnName("ShippingStreet");
a.Property(addr => addr.City).HasColumnName("ShippingCity");
});
// ❌ 避免: 默认列名太长
// ShippingAddress_Street → ShippingStreet3. 初始化集合
csharp
public class Order
{
public int Id { get; set; }
// ✅ 推荐: 始终初始化集合
public List<OrderItem> Items { get; set; } = new();
}
// ❌ 避免: null 集合导致异常
public class Order
{
public List<OrderItem> Items { get; set; } // 可能是 null
}4. 使用值对象模式
csharp
// ✅ 推荐: 不可变值对象
[Owned]
public class Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// EF Core 8+ 支持不可变类型常见陷阱
陷阱 1: 忘记配置 Owned
csharp
// ❌ 错误: 未标记 [Owned],EF Core 尝试创建单独表
public class Address
{
public string Street { get; set; }
}
// 💥 报错: Unable to determine the relationship...
// ✅ 正确: 添加 [Owned] 或使用 OwnsOne
[Owned]
public class Address { ... }陷阱 2: 查询时忘记 Include
csharp
// ❌ 错误: 访问未加载的 Owned Entity
var customer = await context.Customers.FindAsync(1);
var city = customer.ShippingAddress.City; // 💥 可能是 null
// ✅ 正确: Include 加载
var customer = await context.Customers
.Include(c => c.ShippingAddress)
.FirstOrDefaultAsync(c => c.Id == 1);陷阱 3: 共享引用问题
csharp
// ❌ 错误: 两个属性指向同一对象
var customer = new Customer();
var address = new Address { City = "北京" };
customer.ShippingAddress = address;
customer.BillingAddress = address; // 💥 同一引用!
context.Customers.Add(customer);
await context.SaveChangesAsync();
// 数据库中两个地址相同,修改一个会影响另一个
// ✅ 正确: 创建独立副本
customer.ShippingAddress = new Address { City = "北京" };
customer.BillingAddress = new Address { City = "北京" };陷阱 4: 集合性能问题
csharp
// ⚠️ 注意: OwnsMany 在大数据量时可能慢
var order = await context.Orders
.Include(o => o.Items) // 💥 如果有 1000+ 订单项
.FirstOrDefaultAsync(o => o.Id == 1);
// ✅ 解决: 分页加载或投影查询
var items = await context.Orders
.Where(o => o.Id == 1)
.SelectMany(o => o.Items)
.Skip(page * pageSize)
.Take(pageSize)
.ToListAsync();总结
核心要点
- Owned Entity: 无ID的值对象,依赖所有者
- 配置方式:
[Owned]特性或OwnsOne/OwnsMany - 存储策略: 默认在所有者表,也可单独表
- 嵌套支持: 可以多层嵌套值对象
- 集合支持:
OwnsMany映射值对象集合
使用场景对比
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 地址信息 | Owned Entity | 无独立身份 |
| 金额/货币 | Owned Entity | 值语义 |
| 配置对象 | Owned Entity | 依附于主实体 |
| 订单项 | OwnsMany | 值对象集合 |
| 用户资料 | 单独实体 | 可能需要独立查询 |
| 产品分类 | 单独实体 | 可能被多个产品共享 |
代码模板
csharp
// 模板 1: 简单 Owned Entity
[Owned]
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
modelBuilder.Entity<Customer>()
.OwnsOne(c => c.Address, a =>
{
a.Property(x => x.Street).HasColumnName("Street");
a.Property(x => x.City).HasColumnName("City");
});
// 模板 2: Owned Entity 集合
[Owned]
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
modelBuilder.Entity<Order>()
.OwnsMany(o => o.Items, oi =>
{
oi.WithOwner().HasForeignKey("OrderId");
oi.Property<int>("Id");
oi.HasKey("Id");
});