在 DDD 架构中,领域聚合根主键的工程化设计思路


一、为什么「聚合根主键」值得单独设计?

在很多项目中,主键往往被当成数据库层的细节

  • int identity
  • bigint auto_increment
  • 或者一个随手用的 Guid

但在 DDD(领域驱动设计) 中,聚合根是领域模型的核心
聚合根的主键 = 领域中“身份”的表达

如果主键设计得随意,往往会带来这些问题:

  • 领域层被数据库实现细节污染
  • 业务语义无法体现(OrderId / AssetId 全是 Guid
  • 跨聚合、跨上下文传参极易出错
  • 后期想重构、拆服务成本极高

一句话总结:

主键不是“存储编号”,而是领域身份(Identity)的一部分。


二、DDD 中聚合根主键的几个核心原则

1️⃣ 主键属于「领域概念」,不是数据库概念

在 DDD 中:

  • 聚合根存在于 Domain 层
  • Domain 层 不应该依赖数据库
  • 因此,主键 不能体现数据库生成策略

这意味着:

❌ 不推荐

public int Id { get; set; }

❌ 不推荐

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }

这些都把 “怎么存” 强行带进了 “是什么”


2️⃣ 不要让所有聚合根的主键「长得一样」

很多系统里会看到这种代码:

public class Order
{
    public Guid Id { get; private set; }
}

public class Asset
{
    public Guid Id { get; private set; }
}

问题在于:

  • Order.IdAsset.Id 在类型系统里是完全一样的
  • 编译器无法阻止你把 Asset.Id 传给 Order
  • 所有约束只能靠“人记住”

这不是工程化设计,而是祈祷式设计。


三、工程化推荐方案:Strongly Typed ID(强类型主键)

✅ 1️⃣ 为每个聚合根定义专属 ID 类型

public sealed record AssetId(Guid Value);
public sealed record OrderId(Guid Value);

然后在聚合根中使用:

public class Asset : AggregateRoot<AssetId>
{
    public AssetId Id { get; private set; }

    private Asset() { }

    public Asset(AssetId id)
    {
        Id = id;
    }
}

这样做带来的好处

  • 编译期类型安全
  • 不同聚合根的 ID 无法混用
  • 方法签名即业务语义
  • Domain 层完全不关心数据库

四、为什么不推荐在 DDD 中使用「自增主键」

❌ 1️⃣ 自增主键是“存储顺序”,不是“业务身份”

自增 ID 的本质:

  • 依赖数据库
  • 依赖写入顺序
  • 在分布式环境下天然不友好

而领域中的身份应该是:

  • 与存储方式无关
  • 可提前生成
  • 可在内存中完成建模与校验

❌ 2️⃣ 自增主键会反向影响领域建模

典型问题包括:

  • 聚合创建必须“先落库”
  • 领域事件必须等保存后才能发
  • 很难做离线建模、测试或并发构造

这与 DDD 的目标是完全相反的


五、Strongly Typed ID 如何与 EF Core 协作?

这是很多人真正卡住的地方。

1️⃣ 值对象映射(ValueConverter)

builder.Property(x => x.Id)
    .HasConversion(
        id => id.Value,
        value => new AssetId(value)
    );

2️⃣ 数据库里依然是 Guid / bigint

  • 数据库不需要知道 AssetId
  • ORM 负责「领域 ↔ 存储」转换
  • 领域模型保持纯净

这正是 Infrastructure 层存在的意义


六、主键生成策略建议

推荐方案

public static class AssetIdGenerator
{
    public static AssetId New() => new AssetId(Guid.NewGuid());
}
  • 主键在 领域或应用层生成
  • 不依赖数据库
  • 易测试、易扩展

是否一定要 Guid?

不一定:

  • Guid
  • Snowflake
  • ULID
  • 自定义编码(只要不依赖 DB)

关键不是值的形式,而是:领域是否掌控它。


七、聚合根主键设计的一句话总结

在 DDD 架构中, 聚合根主键不是“表的 ID”, 而是“领域身份的类型化表达”。

如果一个系统里:

  • 所有聚合根都是 int Id
  • Domain 层知道自增、序列、数据库策略
  • ID 在服务间随便传

那它大概率只是:

“披着 DDD 外衣的 CRUD 系统”。


八、结语

主键设计看似是一个小点, 但它往往决定了:

  • 领域是否真正独立
  • 架构是否能长期演进
  • 团队是否真的理解 DDD

工程化的 DDD,往往从这些“看起来没必要”的设计开始。

微信订阅号二维码