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

Dav*_*New 11 c# domain-driven-design aggregateroot repository-pattern domain-model

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

问题:

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

此时我无法开始使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

题:

  • 你会认为这是在实例创建时生成Guid值的好方法吗?
  • 这个问题有更好的解决方案吗?

nwa*_*ng0 6

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

界面如下:

interface SequenceGenerator {
    long getNextSequence();
}
Run Code Online (Sandbox Code Playgroud)

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

getNextSequence第一次调用时,它100会向allocatedSequence列中写入一个较大的值(比如说)并返回1.下一次调用将返回2,无需访问数据库.当100序列用尽,它会读取和增量allocatedSequence100一次.

看一下SequenceHiLoGeneratorHibernate的源代码.它基本上做了我上面描述的.


tne*_*tne 6

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

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

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

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

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

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

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

很多时候,应用程序中的“创建”和“修改”功能是截然不同的,您总是首先在数据库中添加实体的记录,然后再检索它们以修改它们。

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

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

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

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

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


Wik*_*hla 5

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

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

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

我知道的另一种选择是您所描述的。


Ily*_*kin 5

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

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

你会认为这是在实例创建时生成Guid值的好方法吗?

如果您不使用任何ORM,您的方法就足够了.特别是,当身份地图工作单元的实施是你的respomibility.但你只修好Equals(object obj)了.GetHashCode()方法不检查是否uniqueId.Equals(default(IdType)).

我建议你研究一下像Sharp-Architecture这样的开源"Infrastructure Boilerplate",并检查它们对所有域实体的基类的实现.

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