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

20

在领域驱动设计中,实体的一个定义特征是它具有唯一标识。

问题:

我无法在实例创建时为实体提供唯一标识。这个标识只有在实体持久化之后由仓储库提供(这个值从底层数据库提供)。

我不能使用 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。 - Adrian Thompson Phillips
Entity Framework 6在Azure SQL数据库上运行。我不认为在插入数据之前可以获取ID。 - Dave New
1
为什么在实体持久化之前需要一个ID,为什么必须依赖于数据库提供的值?请提供更多关于您领域的细节,因为您的假设可能是错误的。 - Bartłomiej Szypelow
@BartłomiejSzypelow:因为实体需要具有身份,有时甚至从实例化的角度来看也是如此。我可以依赖于引用,但我正在开发一个分布式系统,因此实体的身份需要通过Id进行限定(因为序列化将会发生)。我正在尝试实现早期身份生成(请查看Vaughn Vernon的书籍:实现领域驱动设计)。 - Dave New
你提到了分布式。这是使用GUID的另一个原因。正如你所提到的IDDD,书中还有一章介绍了使用模拟Oracle SEQUENCE表进行早期标识生成的黑客技巧。我不喜欢这种方法,但你呢? - Bartłomiej Szypelow
我非常好奇: 你是如何让你的 repo 在插入后设置私有的 uniqueId 字段的? 而且当更新时,你的 repo 如何访问该字段,以知道要更新哪一行? - Timo
5个回答

10

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

该接口如下:

interface SequenceGenerator {
    long getNextSequence();
}

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

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

可以查看Hibernate源代码中的SequenceHiLoGenerator,它基本上就是我所描述的。


7
我认为解决这个问题其实非常简单:
- 如你所述,实体必须有一个身份标识。 - 根据你(完全正确的)要求,实体的身份标识由DBMS集中分配。 - 因此,任何尚未分配身份标识的对象都不是实体。
你正在处理一种数据传输对象类型,它没有身份标识。你应该将其视为通过存储库(在此需要作为身份分配的接口)将数据从您使用的任何输入系统传输到域模型中的对象。我建议您为这些对象创建另一种类型(没有键),并将其传递给存储库的Add/Create/Insert/New方法。
当数据不需要进行太多预处理(即不需要经过多次传递)时,有些人甚至省略了DTO,并直接通过方法参数传递各个数据片段。这确实是您应该考虑此类DTO的方式:作为方便的参数对象。再次注意缺少“键”或“id”参数。
如果您需要在将其插入数据库之前将对象作为实体进行操作,则DBMS序列是您唯一的选择。请注意,这通常相对较少见,您可能需要这样做的唯一原因是如果这些操作的结果最终修改了对象状态,则必须进行第二次请求以更新它,在这种情况下,您肯定会希望避免。
在应用程序中,通常“创建”和“修改”功能足够不同,以至于您总是首先将实体记录添加到数据库中,然后稍后再检索它们以进行修改。
您无疑会担心代码重用。根据您构建对象的方式,您可能需要将某些验证逻辑分离出来,以便存储库可以在将数据插入数据库之前验证数据。请注意,如果使用DBMS序列,则通常不需要这样做,这可能是为什么有些人即使不严格需要它们也系统地使用它们的原因之一。根据您的性能要求,考虑上面的评论,因为序列将生成一个额外的往返,您通常可以避免。
示例:创建一个验证器对象,您可以在实体和存储库中同时使用。
免责声明:我对规范DDD没有深入了解,我不知道这是否真正是推荐的方法,但我认为这很有意义。
我还要补充说,我认为基于对象表示实体或简单数据对象而更改Equals(和其他方法)的行为并不理想。使用您的技术,您还需要确保您用于键的默认值在所有域逻辑中都被正确排除在值域之外。
如果您仍然想使用那种技术,我建议为键使用专用类型。这种类型将用额外的状态对键进行包装,以指示该键是否存在。请注意,此定义与 Nullable 非常相似,因此我会考虑使用它(您可以在 C# 中使用 type? 语法)。采用这种设计,更清楚地表明您允许对象没有标识(空键)。同时也更容易理解为什么这种设计并不理想(再次强调,这是我的个人观点):您正在使用相同的类型来表示实体和无身份数据传输对象。

6

我无法在实例创建时为实体提供唯一标识。这个标识只有在实体被持久化后(从底层数据库提供该值)才会被提供。

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

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

如果您不使用任何ORM,那么您的方法已经足够好了。特别是当identity mapunit of work的实现是您的职责时。但是您仅固定了Equals(object obj)GetHashCode()方法没有检查uniqueId.Equals(default(IdType))

我建议你研究一下任何开源的“基础架构模板”,比如Sharp-Architecture,并查看它们对所有领域实体的基类的实现

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


5
我无法立即开始使用Guid值。
是的,你可以考虑另一种方案。在此方案中,Guid将不是数据库主键,而是在领域模型层面上使用的。你甚至可以拥有两个不同的模型——一个持久化模型,其中int作为主键和guid作为属性,另一个模型是领域模型,在其中guid扮演标识符的角色。
这样,你的领域对象可以在创建后获得其标识,持久性只是较小的业务问题之一。
我所知道的另一种选项就是你所描述的选项。

但是,GUID值如何在物理边界上保持一致?如果实体在两个不同的系统或不同时检索,则它们的GUID将不同。它们需要相同。我确实在存储库实现下拥有单独的持久(EF)模型。感谢您的答案! - Dave New
1
在身份标识持久化之前,无法从相同的存储中检索它。另一方面,临时实体跨越物理边界甚至不需要被持久化也没有问题。在某个时间点上,某个地方的人可以将其持久化,GUID 将成为其身份标识,这正是您在创建时所赋予的 GUID。请注意,它甚至可以被多次持久化,并在不同的系统中获得不同的 int ID,因此 GUID 仍然充当 DDD 身份标识,而 int ID 只是持久化 ID。 - Wiktor Zychla
那么你的意思是说,今天加载的同一实体在明天加载时可能具有不同的标识符? - Dave New
不,身份标识(guid)不会改变。数据库标识会改变,它是持久化数据的系统本地的。无论如何,你都无法避免使用guid。 - Wiktor Zychla

3
你的解决方案非常有效,根据我的经验,我经常使用这种方法。
请注意,将自动递增的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而不是GUID),又具有数据库友好性(小,快速且上升),请尝试我的建议。

我警告您,正确地实现可能会很棘手,特别是涉及到冲突和线程安全性。我还没有发布任何代码。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接