ORM实体与DDD实体

3

我熟悉典型的分层架构,包括服务、实体和存储库。服务操作已注释的实体类,这些类由存储库持久化。在此模型中,实体类只是带有一堆getter和setter的无血统数据容器。业务逻辑驻留在过程化服务类中(由Spring容器管理的单例)。

我正在作为一项业余项目了解领域驱动设计(DDD)。在DDD中,实体形成一个丰富的领域模型,在聚合根方法(和值对象)中容纳了大部分业务逻辑。服务几乎只是协调各种实体、存储库和其他服务的工具。这个丰富的领域模型以真正的面向对象(OOP)方式强制执行业务约束和不变量,促进了代码的可维护性。此外,领域模型是"六边形体系结构"的核心,意味着它只是普通旧 Java 对象(POJO),在源代码级别上不依赖于技术或框架问题。

但是,JPA规范要求实体bean应该具有公共的getter和setter,本质上是一个无血统的数据容器,与DDD领域模型相对冲突。所以我应该在JPA实体中捆绑领域逻辑吗?还是在使用ORM处理DDD时应该维护两个不同的模型和映射逻辑?这些模型和映射逻辑应该在项目级别的哪里存放?


非常好的问题,我开始研究DDD,当我看到这个领域时...我想知道如何在实体模型ORM中处理它。 - Raúl Garcia
2个回答

6
为了保持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);
    }
}

我正在使用Spring Data JPA。那么将toEntity方法包含到领域模型类中是否可行?这会增加一些基础设施的耦合性。或者,可以使用包保护字段,并在基础设施层上具有相同包名称的外部实体构建器类中访问这些字段吗? - Tuomas Toivonen
@TuomasToivonen 我的回答太长了,无法在评论中展示,请查看编辑。 - mtj
1
@mtj,关于你的例子,为什么要破坏领域模型类呢?你可以创建一个 InvoiceMapper 类作为基础设施的一部分,用来将领域模型与数据传输对象进行转换。这样,基础设施就依赖于领域模型,而不是相反。 - Andrea Damiani
1
@damsen 问题在于,领域实体的外部接口不一定公开足够的信息来恢复其状态。因此,您要么使用映射到/从外部表示形式的方式破坏领域,要么将另一个类(如您提到的映射器)授予访问内部结构的权限,但该类并不属于该领域。这两种方法都很丑陋。 - mtj
@mtj 我明白你的意思了,谢谢澄清! :) - Andrea Damiani
显示剩余6条评论

0
关于您的报价:
但是,JPA规范要求实体bean应该有公共的getter和setter方法,这本质上是一种无血缘关系的数据容器,与DDD领域模型相反。
我不是这个主题的专家,但据我所知,JPA并不强制要求公共的getter和setter方法,尽管有时文档可能含糊不清。
例如,Hibernate 5.6文档中说:
声明持久属性的getter和setter JPA规范要求这样做,否则,模型将防止从实体本身外部直接访问实体持久状态字段。
通过“上述要求”,他们可能指的是这个:
实体的持久状态由实例变量表示,这些变量可能对应于JavaBean风格的属性。实例变量必须仅由实体实例自身的方法直接访问。实体的状态仅通过实体的访问器方法(getter/setter方法)或其他业务方法向客户端提供。 这些文本并不是非常清楚,是否始终需要使用setter和setter,即使使用了“requires”这个词。我认为它基本上是说(或应该说),如果您的客户端想要读取和写入属性,则应分别使用公共getter和setter。(使用“您的客户端类”,我指的是您编写的类,而不是Hibernate本身作为实体的客户端。)但是,如果您的客户端不需要读取或写入它,则根本不需要访问器方法,只要使用JPA字段访问属性注释(而不是JPA方法/属性访问属性注释)。除了setter之外,您还可以使用构造函数参数来设置非空属性。(我在我的Hibernate代码中有很多这样的情况。)尽管JPA / Hibernate文档最初可能是针对更贫血的实体类编写的,但您不需要具有这种贫血的访问器。
关于您的报价:
那么我应该将领域逻辑捆绑在JPA实体内吗?还是在使用ORM和DDD时保持两个不同的模型和映射逻辑?这些模型和映射逻辑应该在项目级别的哪里?
制作两个不同的模型会创建很多样板代码。 JPA规范说:
除了返回和设置实例的持久状态外,属性访问器方法还可以包含其他业务逻辑,例如执行验证。当使用基于属性的访问时,持久性提供程序运行时执行此逻辑。
因此,在实体类中具有业务逻辑不应该是一个问题。在理想情况下,实体类是纯DDD领域类,其中应用于它的ORM仅限于元数据(例如注释),而代码本身是ORM无关的。

但是,将许多ORM元数据与大量业务逻辑混合在一起可能会影响可读性。解决这个问题的方法可以是使用Kotlin(一种基于Java的语言)中的扩展方法。 Kotlin扩展方法基本上是Java中的静态方法,可以作为Kotlin中的实例方法使用,并且从逻辑上添加到其所属的类之外。

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

出于某种原因,您想向其中添加persist()方法,但是由于关注点分离,您不希望将其放在Question类中,则可以使用扩展方法来完成此操作:

在文件com.stackoverflow.persistence.QuestionExt.kt中:

package com.stackoverflow.persistence
import com.stackoverflow.domain.Question

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

然后客户端代码可以这样做:

package com.stackoverflow.persistence.repositories

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

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