首页 > 解决方案 > 带有 Spring Data 和 JPA + Hibernate 身份的 DDD 实现问题

问题描述

所以我第一次尝试在一个不太复杂的项目中通过将我的所有代码分成应用程序基础设施接口包来实现域驱动设计。

我还采用了 JPA 实体到域模型的整体分离,它将我的业务逻辑保持为丰富的模型,并使用 Builder 模式进行实例化。这种方法让我很头疼,并且无法弄清楚在将 JPA + ORM 和 Spring Data 与 DDD 一起使用时我是否做错了。

流程说明 该应用程序是一个 Rest API 消费者(无需任何用户交互),每天通过 Scheduler 任务处理相当大量的数据资源,并将其存储或更新到 MySQL。我使用 RestTemplate 获取 JSON 响应并将其转换为域对象,并从那里应用域本身内的任何业务逻辑,例如验证、事件等

从我读过的内容来看,聚合根对象在其整个生命周期中应该有一个标识,并且应该是唯一的。我使用了其余 API 对象的 id,因为它已经是我用来在我的业务领域中识别和跟踪的东西。我还为 Technical id 创建了一个属性,因此当我将实体转换为域对象时,它可以保存更新过程的引用。

当我第一次需要将域持久化到数据源(MySQL)时,我将它们转换为实体对象并使用该save()方法持久化它们。到目前为止,一切都很好。

现在,当我需要更新数据源中的这些记录时,我首先将它们作为员工列表从数据源中获取,将实体对象转换为域对象,然后从其余 API 中获取员工列表作为域模型。到目前为止,我有两个与List<Employee>. 我正在使用 Streams 对它们进行迭代,并检查对象是否不在equal()它们之间,如果是,则将 List 项的集合创建为第三个列表,其中包含需要更新的 Employee 对象。在这里,我已经将技术 ID 传递给了第三个员工列表中的域对象,因此 Hibernate 可以识别并使用它来更新已经存在的记录。

到这里为止都是相当简单的东西,直到我使用该saveAll()方法更新记录。

问题

用代码解释的简单类

EmployeeDO.java

@Entity
@Table(name = "employees")
public class EmployeeDO implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public EmployeeDO() {}

    ...omitted getter/setters
}

雇员.java

public class Employee {

    private Long persistId;
    private Long employeeId;

    private String name;

    private Employee() {}

    ...omitted getters and Builder
}

EmployeeConverter.java

public class EmployeeConverter {

    public static EmployeeDO serialize(Employee employee) {
        EmployeeDO target = new EmployeeDO();

        if (employee.getPersistId() != null) {
          target.setId(employee.getPersistId());
        }

        target.setName(employee.getName());

        return target;
    }

    public static Employee deserialize(EmployeeDO employee) {
        return new Country.Builder(employee.getEmployeeId)
                .withPersistId(employee.getId()) //<-- Technical ID setter
                .withName(employee.getName())
                .build();
    }
}

EmployeeRepository.java

@Component
public class EmployeeReporistoryImpl implements EmployeeRepository {

    @Autowired
    EmployeeJpaRepository db;

    @Override
    public List<Employee> findAll() {
        return db.findAll().stream()
                .map(employee -> EmployeeConverter.deserialize(employee))
                .collect(Collectors.toList());
    }

    @Override
    public void saveAll(List<Employee> employees) {
        db.saveAll(employees.stream()
                .map(employee -> EmployeeConverter.serialize(employee))
                .collect(Collectors.toList()));

    }

}

EmployeeJpaRepository.java

@Repository
public interface EmployeeJpaRepository extends JpaRepository<EmployeeDO, Long> {

}

标签: javahibernatespring-bootjpadomain-driven-design

解决方案


我在我的项目中使用相同的方法:域和持久性的两个不同模型。

首先,我建议您不要使用转换器方法,而是使用Memento模式。您的域实体导出一个纪念品对象,并且可以从同一个对象中恢复它。是的,域有 2 个与域无关的函数(它们的存在只是为了提供非功能性需求),但是另一方面,您避免暴露域业务逻辑从不的函数、getter 和构造函数利用。

关于持久性的部分,我不完全出于这个原因使用 JPA:您必须编写大量代码才能正确地重新加载、更新和持久化实体。我直接编写 SQL 代码:我可以快速编写和测试它,一旦它工作,我确信它会做我想要的。使用 Memento 对象,我可以直接获得我将在插入/更新查询中使用的内容,并且我避免了很多关于 JPA 处理复杂表结构的麻烦。

无论如何,如果您想使用 JPA,唯一的解决方案是:

  • 加载持久性实体并将它们转换为实体
  • 根据您必须在域中进行的更改来更新域实体
  • 保存实体,这意味着:
    • 重新加载持久性实体
    • 更改,或者如果有新的,则使用您从更新的实体中获得的更改创建它们
    • 保存持久性实体

我尝试了一个混合解决方案,其中实体由持久性实体扩展(做起来有点复杂)。应该非常小心地避免模型应该适应来自持久性模型的 JPA 的限制。

这里有一篇关于拆分两个模型的有趣读物。

最后,我的建议是考虑域的复杂程度,并使用最简单的解决方案来解决问题:

  • 它很大并且有很多复杂的行为吗?预计它会长大吗?使用domainpersistence两种模型,直接用 SQL 管理持久化,避免了 read/update/save 阶段的大量 caos。

  • 简单吗?那么,首先,我应该使用 DDD 方法吗?如果真的是,我会让 JPA 注释在domain内拆分。是的,它不是纯粹的 DDD,但我们生活在现实世界中,以纯粹的方式做一些简单的事情的时间不应该比我需要做一些妥协的时间大几个数量级。而且,另一方面,我可以将所有这些东西写在基础设施层的 XML 中,避免用它弄乱。正如这里的春季 DDD 示例中所做的那样


推荐阅读