在域驱动设计中,实体的一个定义特征是它具有身份。
问题:
我无法在实例创建时为实体提供唯一标识。一旦实体被持久化(此值由底层数据库提供),此标识仅由存储库提供。
我无法开始使用 Guid
此时的价值观。现有数据存储在 int
主键值,我无法在实例化时生成唯一的int。
我的解决方案
- 每个实体都有一个标识值
- 一旦持久化(由数据库提供),身份仅设置为真实身份
- 在持久性之前实例化时,标识设置为默认值
- 如果标识是默认标识,则实体可通过引用进行比较
- 如果标识不是默认标识,则实体可通过标识值进行比较
代码(所有实体的抽象基类):
public abstract class Entity<IdType>
{
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<IdType>;
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))
。
我建议你研究任何开源的“Infrastructure Boilerplate” 夏普架构 并检查他们的 为所有域实体实现基类。
我习惯于编写自定义的实现 Equals()
对于域实体,但在使用ORM时它可能是多余的。如果您使用任何ORM,它提供的实现 身份地图 和 工作单位 开箱即用的模式,你可以依靠它们。
您可以使用序列生成器生成唯一 int
/long
实例化实体对象时的标识符。
界面如下:
interface SequenceGenerator {
long getNextSequence();
}
序列生成器的典型实现使用数据库中的序列表。序列表包含两列: sequenceName
和 allocatedSequence
。
什么时候 getNextSequence
被称为第一次,它写了一个很大的值(比方说 100
)到 allocatedSequence
列和返回 1
。下一个电话将返回 2
无需访问数据库。当。。。的时候 100
序列耗尽,它读取并递增 allocatedSequence
通过 100
再次。
看看吧 SequenceHiLoGenerator
在Hibernate源代码中。它基本上做了我上面描述的。
此时我无法开始使用Guid值。
是的,你可以,那将是另一种选择。 Guids不是您的数据库主键,而是在域模型级别使用。在这种方法中,您甚至可以拥有两个独立的模型 - 一个持久化模型,其中int作为主键,guid作为属性,另一个模型是域模型,其中guids扮演标识符的角色。
通过这种方式,您的域对象可以在创建后获取其身份,而持久性只是次要业务问题之一。
我知道的另一个选项是你描述的那个。
我相信解决方案实际上相当简单:
你在这里处理的是一种数据传输对象类型,它没有标识。您应该将其视为通过存储库将您使用的任何输入系统中的数据传输到域模型(您需要将其作为身份分配的接口)。我建议你为这些对象创建另一种类型(一种没有密钥),并将其传递给存储库的Add / Create / Insert / New方法。
当数据不需要太多预处理时(即不需要多次传递),有些人甚至会省略DTO并直接通过方法参数传递各种数据。这就是你应该如何看待这样的DTO:作为方便的参数对象。再次注意缺少“关键”或“id”参数。
如果在将对象插入数据库之前需要将对象作为实体进行操作,那么DBMS序列是您唯一的选择。请注意,这通常是相对罕见的,您可能需要执行此操作的唯一原因是,如果这些操作的结果最终修改了对象状态,那么您必须再次请求在数据库中更新它,您需要我当然更愿意避免。
通常,应用程序中的“创建”和“修改”功能非常独特,您将始终在稍后再次检索实体之前为数据库中的实体添加记录以进行修改。
毫无疑问,您会担心代码重用。根据构造对象的方式,您可能需要将某些验证逻辑分解出来,以便存储库可以在将数据插入数据库之前验证数据。请注意,如果您正在使用DBMS序列,这通常是不必要的,并且可能是某些人系统地使用它们的原因,即使他们并不严格需要它们。根据您的性能要求,请考虑上述注释,因为序列将产生您经常能够避免的额外往返。
免责声明:我对规范DDD没有深入的了解,我不知道这是不是真的如此 推荐的 方法,但它对我有意义。
我还要补充一下,在我看来,改变行为 Equals
(以及其他方法)基于对象是表示实体还是简单数据对象而言根本不理想。使用您使用的技术,还需要确保在所有域逻辑中从值域中正确排除用于密钥的默认值。
如果您仍想使用该技术,我建议使用专用类型的密钥。此类型将使用指示密钥是否存在的附加状态来包装/包装密钥。请注意,此定义类似于 Nullable<T>
这么多,我考虑使用它(你可以使用 type?
C#中的语法。通过这种设计,您可以更清楚地允许对象不具有标识(空键)。为什么设计不理想也应该更加明显(在我看来):你使用相同的类型来表示实体和无身份的数据传输对象。
根据我的经验,您建议的解决方案完全有效。我已经使用了这种方法。
请注意,在外部共享自动增量ID会泄漏有关卷的信息。这有时可能需要额外的GUID属性 - 不是美丽的东西。
单行重写为您的实现
我喜欢整齐地实现一个实体 Equals()
和 GetHashCode()
如下。 (我包括 ToString()
因为我总是覆盖它,以便于调试和记录。)
public override string ToString() => $"{{{this.GetType().Name} Id={this.Id}}}"; // E.g. {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
?如果它在不需要存储库的情况下也是新的怎么办?
我意识到你的桌子目前可能只适合 int
就像它一样 PRIMARY KEY
。但是,如果你能够改变它 long
或者对于你未来的表格,我的建议可能会让你感兴趣。
在 提案:本地唯一的GUID替代方案,我解释了如何构建一个本地唯一的,可更新的,严格提升的64位值。它取代了自动增量ID + GUID组合。
我一直不喜欢同时拥有数字ID和GUID的想法。这就像是说:“这是实体的唯一标识符。而且......这是它的另一个唯一标识符。”当然,你可以保留一个域名和语言,但这会让你遇到同时管理的技术问题 和 隐藏额外的数字ID。如果您希望拥有一个既适合域的ID(没有存储库,也可以是命名ID而不是GUID)的新ID,并且数据库友好(小,快,升),请尝试我的建议。
我警告你,实施起来可能很棘手,特别是在冲突和线程安全方面。我还没有发布任何代码。