【问题标题】:DDD: Entity identity before being persistedDDD:持久化之前的实体身份
【发布时间】:2014-02-10 14:33:24
【问题描述】:

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

问题:

我无法在创建实例时向实体提供唯一身份。此身份仅在实体持久化后由存储库提供(此值由底层数据库提供)。

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

【问题讨论】:

  • 您使用的是什么数据库?如果您使用的是 RavenDB 或 NHibernate,您也许可以利用 HiLo 模式,它允许您在将实体持久保存到数据库之前提前获取 id。
  • 基于 Azure SQL 数据库的实体框架 6。我不相信在插入东西之前有可能得到一个 id。
  • 为什么在实体被持久化之前需要一个 id 以及为什么你必须依赖数据库提供的值?请向我们提供有关您的域的更多详细信息,因为您的假设可能是错误的。
  • @BartłomiejSzypelow:因为实体需要有身份,有时甚至从实例化的角度来看也是如此。我可以只依赖参考,但我正在开发一个分布式系统,因此实体的身份需要由 Id 限定(因为会发生序列化)。我正在尝试实现早期身份生成(查看 Vaughn Vernon 的书:实施领域驱动设计)。
  • 您提到了分布式。 GUID 的另一个原因。正如您提到的 IDDD,还有一章是关于使用模仿 Oracle 的 SEQUENCE 的表进行早期身份生成的黑客攻击。我不喜欢这种方法,但你呢?

标签: c# domain-driven-design repository-pattern domain-model aggregateroot


【解决方案1】:

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

界面如下:

interface SequenceGenerator {
    long getNextSequence();
}

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

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

查看 Hibernate 源代码中的SequenceHiLoGenerator。它基本上完成了我上面描述的操作。

【讨论】:

    【解决方案2】:

    我无法在创建实例时向实体提供唯一身份。此身份仅在实体持久化后由存储库提供(此值由底层数据库提供)。

    您有多少地方可以创建相同类型的实体列表,并且您有多个具有默认 ID 的实体?

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

    如果您不使用任何 ORM,您的方法就足够了。尤其是当您负责实现identity mapunit of work 时。但是您只修复了Equals(object obj)GetHashCode() 方法不检查是否uniqueId.Equals(default(IdType))

    我建议您查看任何开源“基础设施样板”,例如 Sharp-Architecture,并查看他们的 implementation of the base class for all domain entities

    我习惯于为域实体编写 Equals() 的自定义实现,但在使用 ORM 时它可能是多余的。如果您使用任何 ORM,它会提供开箱即用的 identity mapunit of work 模式的实现,您可以依赖它们。

    【讨论】:

      【解决方案3】:

      我相信这个问题的解决方案实际上相当简单:

      • 正如你提到的,实体必须有一个身份,

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

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

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

      当数据不需要太多预处理(即不需要太多传递)时,有些人甚至会省略 DTO,直接通过方法参数传递各种数据。这确实是您应该如何看待此类 DTO:作为方便的参数对象。同样,请注意缺少“key”或“id”参数。

      如果您需要在将对象插入数据库之前将其作为实体进行操作,那么 DBMS 序列是您唯一的选择。请注意,这通常比较少见,您可能需要这样做的唯一原因是,如果这些操作的结果最终会修改对象状态,那么您必须再次请求在数据库中更新它,您肯定宁愿避免。

      通常,应用程序中的“创建”和“修改”功能截然不同,因此您总是先在数据库中添加实体的记录,然后再检索它们以进行修改。

      您无疑会担心代码重用。根据您构建对象的方式,您可能需要排除一些验证逻辑,以便存储库可以在将数据插入数据库之前对其进行验证。请注意,如果您使用 DBMS 序列,这通常是不必要的,这可能是某些人系统地使用它们的原因,即使他们并不严格需要它们。根据您的性能要求,将上述 cmets 考虑在内,因为序列会产生您通常可以避免的额外往返行程。

      • 示例:创建一个在实体和存储库中都使用的验证器对象。

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

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

      如果您仍想使用该技术,我建议使用专用类型的键。这种类型会用额外的状态来装箱/包装密钥,指示密钥是否存在。请注意,此定义与 Nullable&lt;T&gt; 非常相似,因此我会考虑使用它(您可以在 C# 中使用 type? 语法)。通过这种设计,更清楚的是您允许对象没有身份(空键)。设计不理想的原因也应该更明显(在我看来,再次强调):您使用相同的类型来表示实体和无身份数据传输对象。

      【讨论】:

        【解决方案4】:

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

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

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

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

        【讨论】:

        • 但是 Guid 值如何跨物理边界保持一致?如果在两个不同的系统中或在不同的时间检索实体,它们的 Guid 将不同。他们需要是一样的。我在存储库实现下确实有一个单独的持久性 (EF) 模型。感谢您的回答!
        • 在身份持久化之前,无法从同一个存储中检索。另一方面,瞬态实体在没有被持久化的情况下跨越物理边界并没有错。在某个时刻,有人可以坚持它,而 guid 就是它的身份,就是你在出生时给它的那个 guid。请注意,它甚至可以被持久化多次并在不同的系统上获得不同的 int ID,因此 guid 仍然充当 DDD 身份,其中 int ID 只是持久性 ID。
        • 那么你是说今天加载的同一个实体明天加载时可能有不同的身份?
        • 不,身份 (guid) 不会改变。数据库身份发生变化,并且它在持久化数据的系统中是本地的。反正你也逃不掉的。
        【解决方案5】:

        根据我的经验,您建议的解决方案是完全有效的。这种方法我用过不少。

        请注意,在外部共享自动增量 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 == default ? RuntimeHelpers.GetHashCode(this) : this.Id.GetHashCode();
        

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

        替代解决方案

        如果您想要更好的东西,这里有另一个建议。如果您的值(出于我们的意图和目的)与 GUID 一样好,但适合 long,该怎么办?如果它也是新的而不需要存储库怎么办?

        我意识到您的桌子目前可能只适合int 作为它的PRIMARY KEY。但是,如果您能够将其更改为 long,或者为您未来的表格,我的建议可能会引起您的兴趣。

        Proposal: locally unique GUID alternative 中,我解释了如何构建一个本地唯一的、可更新的、严格升序的 64 位值。它取代了自增 ID + GUID 组合。

        我一直不喜欢同时拥有数字 ID 和 GUID 的想法。这就像说:“这是实体的唯一标识符。而且……这是它的另一个唯一标识符。”当然,您可以将一个排除在域和语言之外,但这会给您带来同时管理 隐藏额外数字 ID 的技术问题。如果您希望拥有一个对域友好(无需存储库的新 ID,以及命名 ID 而不是 GUID)和对数据库友好(小、快和升序)的单一 ID,请尝试我的建议。

        我警告您,要正确执行该实现可能会很棘手,尤其是在冲突和线程安全方面。我还没有发布任何代码。

        【讨论】:

          猜你喜欢
          • 2023-03-27
          • 2020-05-18
          • 2021-04-10
          • 2013-03-25
          • 1970-01-01
          • 1970-01-01
          • 2013-08-13
          • 2012-04-07
          • 1970-01-01
          相关资源
          最近更新 更多