DDD:持久化之前的实体身份

在域驱动设计中,实体的一个定义特征是它具有身份。

问题:

我无法在实例创建时为实体提供唯一标识。 一旦实体被持久化(此值由底层数据库提供),此标识仅由存储库提供。

此时我无法开始使用Guid值。 现有数据与int主键值一起存储,我无法在实例化时生成唯一的int。

我的解决方案

  • 每个实体都有一个标识值
  • 一旦持久化(由数据库提供),身份仅设置为真实身份
  • 在持久性之前实例化时,标识设置为默认值
  • 如果标识是默认标识,则实体可通过引用进行比较
  • 如果标识不是默认标识,则实体可通过标识值进行比较

代码(所有实体的抽象基类):

 public abstract class Entity { private readonly IdType uniqueId; public IdType Id { get { return uniqueId; } } public Entity() { uniqueId = default(IdType); } public Entity(IdType id) { if (object.Equals(id, default(IdType))) { throw new ArgumentException("The Id of a Domain Model cannot be the default value"); } uniqueId = id; } public override bool Equals(object obj) { if (uniqueId.Equals(default(IdType))) { var entity = obj as Entity; if (entity != null) { return uniqueId.Equals(entity.Id); } } return base.Equals(obj); } public override int GetHashCode() { return uniqueId.GetHashCode(); } } 

题:

  • 您是否认为这是在实例创建时生成Guid值的好方法?
  • 这个问题有更好的解决方案吗?

我无法在实例创建时为实体提供唯一标识。 一旦实体被持久化(此值由底层数据库提供),此标识仅由存储库提供。

您在哪里创建了相同类型的实体列表,并且您有多个具有默认ID的实体?

您是否认为这是在实例创建时生成Guid值的好方法?

如果您不使用任何ORM,您的方法就足够了。 特别是,当身份地图和工作单元的实施是你的respomibility。 但是你只修复了Equals(object obj)GetHashCode()方法不检查uniqueId.Equals(default(IdType))

我建议你研究一下像Sharp-Architecture这样的开源“Infrastructure Boilerplate”,并检查它们对所有域实体的基类的实现 。

我习惯为域实体编写Equals()自定义实现,但在使用ORM时它可能是多余的。 如果你使用任何ORM,它提供开箱即用的身份地图和工作单元模式的实现,你可以依赖它们。

在实例化实体对象时,可以使用序列生成器生成唯一的int / long标识符。

界面如下:

 interface SequenceGenerator { long getNextSequence(); } 

序列生成器的典型实现使用数据库中的序列表。 序列表包含两列: sequenceNameallocatedSequence

当第一次调用getNextSequence时,它会将一个较大的值(比如100 )写入getNextSequence列并返回1 。 下一个调用将返回2而无需访问数据库。 当100序列用完时,它会再次读取并将allocatedSequence递增100

看一下Hibernate源代码中的SequenceHiLoGenerator 。 它基本上做了我上面描述的。

此时我无法开始使用Guid值。

是的,你可以,那将是另一种选择。 Guids不是您的数据库主键,而是在域模型级别使用。 在这种方法中,您甚至可以拥有两个独立的模型 – 一个持久化模型,其中int作为主键,guid作为属性,另一个模型是域模型,其中guids扮演标识符的角色。

通过这种方式,您的域对象可以在创建后获取其身份,而持久性只是次要业务问题之一。

我知道的另一个选项是你描述的那个。

我相信解决方案实际上相当简单:

  • 如你所述,实体必须具有身份,

  • 根据您的(完全有效)要求,您的实体的身份由DBMS集中分配,

  • 因此,尚未分配身份的任何对象都不是实体。

你在这里处理的是一种数据传输对象类型,它没有标识。 您应该将其视为通过存储库将您使用的任何输入系统中的数据传输到域模型(您需要将其作为身份分配的接口)。 我建议你为这些对象创建另一种类型(一种没有密钥),并将其传递给存储库的Add / Create / Insert / New方法。

当数据不需要太多预处理时(即不需要多次传递),有些人甚至会省略DTO并直​​接通过方法参数传递各种数据。 这就是你应该如何看待这样的DTO:作为方便的参数对象。 再次注意缺少“关键”或“id”参数。

如果在将对象插入数据库之前需要将对象作为实体进行操作,那么DBMS序列是您唯一的选择。 请注意,这通常是相对罕见的,您可能需要执行此操作的唯一原因是,如果这些操作的结果最终修改了对象状态,那么您必须再次请求在数据库中更新它,您需要我当然更愿意避免。

通常,应用程序中的“创建”和“修改”function非常独特,您将始终在稍后再次检索实体之前为数据库中的实体添加记录以进行修改。

毫无疑问,您会担心代码重用。 根据构造对象的方式,您可能需要将某些validation逻辑分解出来,以便存储库可以在将数据插入数据库之前validation数据。 请注意,如果您正在使用DBMS序列,这通常是不必要的,并且可能是某些人系统地使用它们的原因,即使他们并不严格需要它们。 根据您的性能要求,请考虑上述注释,因为序列将产生您经常能够避免的额外往返。

  • 示例:创建在实体和存储库中使用的validation程序对象。

免责声明:我对规范DDD没有深入的了解,我不知道这是否真的是推荐的方法,但对我来说是有道理的。

我还要补充一点,在我看来,基于对象是表示实体还是简单数据对象来改变Equals (和其他方法)的行为根本不理想。 使用您使用的技术,还需要确保在所有域逻辑中从值域中正确排除用于密钥的默认值。

如果您仍想使用该技术,我建议使用专用类型的密钥。 此类型将使用指示密钥是否存在的附加状态来包装/包装密钥。 请注意,这个定义类似于Nullable ,以至于我考虑使用它(你可以在C#中使用type?语法)。 通过这种设计,您可以更清楚地允许对象不具有标识(空键)。 为什么设计不理想也应该更加明显(在我看来):你使用相同的类型来表示实体和无身份的数据传输对象。

根据我的经验,您建议的解决方案完全有效。 我已经使用了这种方法。

请注意,在外部共享自动增量ID会泄漏有关卷的信息。 这有时可能需要额外的GUID属性 – 不是美丽的东西。

单行重写为您的实现

我喜欢整齐地实现Entity的Equals()GetHashCode() ,如下所示。 (我包括ToString()因为我总是覆盖它,以便于调试和记录。)

 public override string ToString() => $"{{{this.GetType().Name} Id={this.Id}}}"; // Eg {MyEntity Id=1} (extra brackets help when nesting) public override bool Equals(object obj) => (this.Id == default) ? ReferenceEquals(this, obj) : this.Id == (obj as MyEntity)?.Id; public override int GetHashCode() => this.Id.GetHashCode(); 

ReferenceEquals() vs base.Equals()是一个有趣的讨论。 🙂

替代解决方案

如果你想要更好的东西,这是另一个建议。 如果你有一个值( 对于我们的意图和目的 )和GUID一样好,但是适合long那个怎么办? 如果它在不需要存储库的情况下也是新的怎么办?

我意识到你的表目前可能只适合作为其PRIMARY KEYint 。 但是如果你能够将其改为long或未来的表格,我的建议可能会让你感兴趣。

在Proposal:本地唯一的GUID替代方案中 ,我解释了如何构建本地唯一的,可更新的,严格提升的64位值。 它取代了自动增量ID + GUID组合。

我一直不喜欢同时拥有数字ID和GUID的想法。 这就像是说:“这是实体的唯一标识符。而且……这是它的另一个唯一标识符。” 当然,您可以保留一个域和语言,但这会让您遇到同时管理隐藏额外数字ID的技术问题。 如果您希望拥有一个既适合域的ID(没有存储库,也可以是命名ID而不是GUID)的新ID,并且数据库友好(小,快,升),请尝试我的建议。

我警告你,实施起来可能很棘手,特别是在冲突和线程安全方面。 我还没有发布任何代码。