ORM 实体与 DDD 实体

ORM Entities vs DDD Entities

我熟悉由服务、实体和存储库组成的典型分层架构。服务操纵由存储库持久化的注释实体 类。在这个模型中,实体 类 只是带有一堆 getter 和 setter 的贫血数据容器。业务逻辑驻留在程序服务 类 中(由 spring 容器管理的单例)。

我正在学习 DDD 作为一个业余项目。在 DDD 中,实体形成了一个丰富的域模型,它在聚合根方法(和值对象)中容纳了大部分业务逻辑。服务几乎只是协作实体、存储库和其他服务的协调者。丰富的域模型强制执行业务约束并以真正的 OOP 方式不变,并提高代码的可维护性。此外,域模型是 六边形架构 的核心,这意味着它只是 POJO,不依赖于源代码级别的技术或框架问题。

但是 JPA 规范要求实体 bean 应该有 public getter 和 setter,本质上是一个贫血的数据容器,与 DDD 领域模型相对立。那么我应该在 JPA 实体中捆绑域逻辑吗?或者在 DDD 上使用 ORM 时,我应该维护两个不同的模型和映射逻辑吗?这些模型和映射逻辑应该位于项目级别的什么位置?

为了维护 DDD,尤其是将域模型与数据库层分离(因为两者可能会单独更改),您需要两个不同的模型。

然后,您需要某种存储库服务,它知道(即依赖于)您的域模型,并且可以进行某种双向映射。在实践中,即使这与纯 DDD 知识背道而驰,您也可能需要在您的域模型中提供某种帮助(即转储已知的内部结构并从此类转储中恢复状态。)但它确实很难存储并在持久存储中恢复 真实 黑盒,除非您想进行简单的对象序列化。


关于评论中的问题:

这正是问题所在:您要么将基础架构混合到域中,要么公开内部数据。不知何故 每个 撰写有关 DDD 的作者只是通过不谈论这个问题而躲过了这颗子弹。这两种变体都同样丑陋,但由于您似乎尝试了一种相当纯粹的 DDD 方法,因此我将创建一个耦合到域对象的 DTO 对象,基础结构层可以访问它(例如,通过使用包保护访问)。但是,我 不会 授予对真实内部值的访问权限。这样你就可以将你的“腐败”限制在一个点上,你可以随意更改实施细节。

将一些伪代码放入答案中:

public class Invoice {   // Domain class
    // implementation details. @Rest of the world: none of your business!
    private Person creditor;        // other domain objects
    private MonetaryAmount amount;  // and value objects

    // Corruption starts here
    toInvoiceDto() {
        InvoiceDto res = new InvoiceDto();
        res.setCreditorId(creditor.getId()); // mapping into external representation
        ...
        return res;
    }

    static Invoice fromInvoiceDto(InvoiceDto persistentSource) {
        ...
    }
    // Corruption ends here

    // do real business :^)
}

public class InvoiceDto {
    ...
}

public class Repository {
    public void saveInvoice(Invoice businessObject) {
       // *very* roughly
       InvoiceDto dto = businessObject.toInvoiceDto();
       InvoiceEntity entity = someKindOfMapper.toEntity(dto);
       entityManager.save(entity);
    }
}

关于您的报价:

But JPA specification mandates the entity bean should have public getters and setters, inherently being an anemic data container, antithesis to the DDD domain model.

我不是这个主题的专家,但据我所知,JPA 不强制要求 public getter 和 setter,尽管文档有时可能会含糊不清。

例如,Hibernate 5.6 文档说:

2.5.4. Declare getters and setters for persistent attributes

The JPA specification requires this, otherwise, the model would prevent accessing the entity persistent state fields directly from outside the entity itself.

对于“上面引用的要求”,他们可能是这个意思:

The persistent state of an entity is represented by instance variables, which may correspond to JavaBean-style properties. An instance variable must be directly accessed only from within the methods of the entity by the entity instance itself. The state of the entity is available to clients only through the entity’s accessor methods (getter/setter methods) or other business methods.

这些文本不是很清楚是否总是需要 setter 和 setter,即使使用了“requires”这个词。我认为它基本上说(或应该说)的是,如果您的客户想要读取和写入属性,那么您应该分别使用 public getter 和 setter。 (对于“your client classes”,我的意思是您编写的 class,而不是 Hibernate 本身作为实体的客户端。)但是如果您的客户不需要读取或写入它,那么您根本不需要访问器方法,只要您使用 JPA field-access 属性注释(相对于 JPA method/property-access 属性注释).除了 setter,您还可以为 non-null 属性使用构造函数参数。 (我的 Hibernate 代码中有很多这样的情况。)虽然 JPA/Hibernate 文档最初可能是在考虑到更贫血的实体 classes 的情况下编写的,但你不需要有这样的贫血访问器。

关于您的报价:

So should I bundle domain logic inside the JPA entity? Or should I maintain two distinct models and the mapping logic when working with ORM on DDD? Where should these model and the mapping logic live at the project level?

制作两个不同的模型会产生大量样板代码。

JPA spec 说:

In addition to returning and setting the persistent state of the instance, property accessor methods may contain other business logic as well, for example, to perform validation. The persistence provider runtime executes this logic when property-based access is used.

因此在实体 classes 中包含业务逻辑应该不是问题。 理想情况下,实体classes是纯DDD领域classes,其中应用的ORM仅限于元数据(例如注解),其本身的代码是ORM不可知论者。

但是将大量 ORM 元数据与大量业务逻辑混合在一起会损害可读性。对此的解决方案可能是 Kotlin(一种基于 Java 的语言)中的扩展方法。 Kotlin 扩展方法基本上是 Java 中的静态方法,可以用作 Kotlin 中的实例方法,它位于 class 外部,它被逻辑添加到其中。

例如,您可以拥有以下实体 class:com.Whosebug.domain.Question

并且无论出于何种原因,您想向其中添加一个 persist() 方法,但您不想将其包含在 Question class 中,因为担心,你可以用扩展方法完成这个:

文件中com.Whosebug.persistence.QuestionExt.kt:

package com.Whosebug.persistence
import com.Whosebug.domain.Question

fun Question.persist() {
    // read "id" property of question object
    val id = this.id
    // persist the question
}

那么客户端代码可以这样做:

package com.Whosebug.persistence.repositories

class QuestionRepository {
    fun save(question: Question) {
        question.persist()
    }
}