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


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

问题:

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

我无法开始使用 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值的好方法?
  • 这个问题有更好的解决方案吗?

3577
2018-01-21 06:22


起源

你用的是什么数据库?如果您正在使用RavenDB或NHibernate,您可以利用HiLo模式,它允许您在将实体持久保存到数据库之前提前获取ID。 - Adrian Thompson Phillips
Azure SQL数据库之上的实体框架6。我不相信在插入内容之前可以获得id。 - davenewza
为什么在实体持久化之前需要id,为什么你必须依赖DB提供的值?请提供有关您域名的更多详细信息,因为您的假设可能是错误的。 - Bartłomiej Szypelow
@BartłomiejSzypelow:因为实体需要有身份,有时甚至是从实例化的角度来看。我可以依赖于引用,但我正在处理分布式系统,因此实体的身份需要由Id限定(因为序列化将发生)。我正在努力实现早期身份生成(请查看Vaughn Vernon的书:实现域驱动设计)。 - davenewza
你提到了分布式。 GUID的另一个原因。正如你提到的IDDD,还有一章关于使用表格模仿Oracle的SEQUENCE进行早期身份生成的黑客攻击。我不喜欢这种方法,但你怎么样? - Bartłomiej Szypelow


答案:


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

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

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

如果您不使用任何ORM,您的方法就足够了。特别是在执行时 身份地图 和 工作单位 是你的respomibility。但你已经修好了 Equals(object obj) 只要。 GetHashCode() 方法不检查是否 uniqueId.Equals(default(IdType))

我建议你研究任何开源的“Infrastructure Boilerplate” 夏普架构 并检查他们的 为所有域实体实现基类

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


5
2018-01-21 22:32





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

界面如下:

interface SequenceGenerator {
    long getNextSequence();
}

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

什么时候 getNextSequence 被称为第一次,它写了一个很大的值(比方说 100)到 allocatedSequence 列和返回 1。下一个电话将返回 2 无需访问数据库。当。。。的时候 100 序列耗尽,它读取并递增 allocatedSequence 通过 100 再次。

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


5
2018-01-21 19:18





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

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

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

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


2
2018-01-21 07:16



但Guid值如何在物理边界上保持一致?如果在两个不同的系统中或在不同的时间检索实体,则它们的Guids将是不同的。他们需要是一样的。我在存储库实现下有一个单独的持久性(EF)模型。感谢你的回答! - davenewza
在标识持久化之前,无法从同一商店检索它。另一方面,瞬态实体在没有持久化的情况下越过物理边界也没有错。在某个地方某个人可以坚持下去,而guid就是它的身份,就像你在出生时给予它的那样。注意,它甚至可以多次持久化并在不同的系统上获得不同的int ID,因此guid仍然充当DDD标识,其中int ID仅是持久性ID。 - Wiktor Zychla
所以你说今天加载的同一个实体在明天加载时会有不同的身份吗? - davenewza
不,身份(指导)不会改变。数据库标识发生更改,它对于持久保存数据的系统是本地的。无论如何你不会逃脱指导。 - Wiktor Zychla


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

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

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

  • 因此,任何尚未赋予身份的对象都是  一个实体。

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

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

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

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

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

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

免责声明:我对规范DDD没有深入的了解,我不知道这是不是真的如此 推荐的 方法,但它对我有意义。

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

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


2
2017-08-18 13:47





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

请注意,在外部共享自动增量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,并且数据库友好(小,快,升),请尝试我的建议。

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


2
2017-10-29 13:29