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

时间:2014-01-21 06:22:10

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

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

问题:

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

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

5 个答案:

答案 0 :(得分:6)

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

界面如下:

interface SequenceGenerator {
    long getNextSequence();
}

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

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

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

答案 1 :(得分:5)

  

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

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

  

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

如果您不使用任何ORM,您的方法就足够了。特别是,当identity mapunit of work的实施是你的respomibility。但是你只修复了Equals(object obj)GetHashCode()方法不会检查uniqueId.Equals(default(IdType))

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

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

答案 2 :(得分:4)

  

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

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

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

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

答案 3 :(得分:2)

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

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

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

  • 因此,尚未分配身份的任何对象一个实体。

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

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

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

通常,应用程序中的“创建”和“修改”功能非常独特,您可以先在数据库中为实体添加记录,然后再重新检索它们以进行修改。

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

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

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

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

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

答案 4 :(得分:2)

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

请注意,从外部共享自动增量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或未来的表格,我的建议可能会引起您的兴趣。

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

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

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