Skip to content

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 → ShippingStreet

3. 初始化集合 ​

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();

总结 ​

核心要点 ​

  1. Owned Entity: 无ID的值对象,依赖所有者
  2. 配置方式: [Owned] 特性或 OwnsOne/OwnsMany
  3. 存储策略: 默认在所有者表,也可单独表
  4. 嵌套支持: 可以多层嵌套值对象
  5. 集合支持: 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");
    });

下一步 ​

基于 MIT 许可发布