Spring Boot:如何从 JPA/Hibernate 注释中保持 DDD 实体的清洁?

Lor*_*ill 17 orm spring domain-driven-design hibernate spring-boot

我正在编写一个希望遵循 DDD 模式的应用程序,一个典型的实体类如下所示:

@Entity
@Table(name = "mydomain_persons")
class Person { 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name="fullname") 
    private String fullName;

    @OneToMany(cascade=ALL, mappedBy="item")
    private Set<Item> items;
}
Run Code Online (Sandbox Code Playgroud)

如您所见,由于 JPA/Hibernate 严重依赖于实体类的注释,我的域实体类现在被持久性感知注释所污染。这违反了 DDD 原则,也违反了层的分离。它还给我带来了与 ORM 无关的属性的问题,例如事件。如果我使用@Transient,它不会初始化事件列表,我必须手动执行此操作,否则会出现奇怪的错误。

我喜欢域实体是 POJO(或 POKO,因为我使用 Kotlin),所以我不想在实体类上有这样的注释。但是,我绝对不希望使用 XML 配置,这很可怕,这也是 Spring 开发人员首先转向注释的原因。

我有哪些可用的选项?我应该定义一个包含此类注释的 DTO 类和一个将每个 DTO 转换为相应域实体的 Mapper 类吗?这是一个好习惯吗?

编辑:我知道在 C# 中,实体框架允许使用配置类在实体类之外创建映射类,这是比 XML 地狱更好的替代方法。我不确定这种技术是否在 JVM 世界中可用,有人知道下面的代码是否可以用 Spring 完成吗?

public class PersonDbContext: DbContext 
{
    public DbSet<Person> People { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    //Write Fluent API configurations here

    //Property Configurations
    modelBuilder.Entity<Person>().Property(p => p.id).HasColumnName("id").IsRequired();
    modelBuilder.Entity<Person>().Property(p => p.name).hasColumnName("fullname").IsRequired();
    modelBuilder.Entity<Person>().HasMany<Item>(p => p.items).WithOne(i => i.owner).HasForeignKey(i => i.ownerid)
}
Run Code Online (Sandbox Code Playgroud)

gfl*_*rio 8

我针对这个问题找到的解决方案是使用抽象域实体,由我的类在持久层实现(可能是也可能不是 Hibernate 实体本身)。这样,我的域类对持久性机制一无所知,我的持久性类对业务逻辑一无所知,并且我基本上避免了映射代码。让我扩展一下:

想象一个这样布局的项目(这几乎是我组织项目的方式):

-
|-business_logic
| |-person
| | |-Person.java
| | |-Item.java  //assuming "item" is inside the Person aggregate
| | |-FullName.java  // Let's make FullName a Value Object.
| | |-DoXWithPersonApplicationService.java
| |-aggregateB
| |-aggregateC
|
|-framework
| |-controllers
| |-repositories
| |-models
| | |-JpaPerson.java
| | |-JpaItem.java
| | |-etc.
Run Code Online (Sandbox Code Playgroud)

那么你的 Person 类可能看起来像这样:

-
|-business_logic
| |-person
| | |-Person.java
| | |-Item.java  //assuming "item" is inside the Person aggregate
| | |-FullName.java  // Let's make FullName a Value Object.
| | |-DoXWithPersonApplicationService.java
| |-aggregateB
| |-aggregateC
|
|-framework
| |-controllers
| |-repositories
| |-models
| | |-JpaPerson.java
| | |-JpaItem.java
| | |-etc.
Run Code Online (Sandbox Code Playgroud)

您的 FullName 类可能如下所示:

public abstract class Person {
    public abstract int getId();

    public abstract FullName getName();
    protected abstract void setName(FullName name);

    public abstract ImmutableSet<Item> getItems(); // Say you're using Guava
    protected abstract void addItem(String itemName, int qtd);
    protected abstract void removeItem(Item item);

    void doBusinessStuff(String businessArgs) {
        // Run complex domain logic to do business stuff.
        // Uses own getters and setters.
    }

}
Run Code Online (Sandbox Code Playgroud)

最后,您的 JpaPerson 类应该类似于:

public final class FullName {

    private final String firstName;
    private final String lastName;

    // Constructors, factories, getters...

}
Run Code Online (Sandbox Code Playgroud)

有几点需要注意:

  1. 任何修改实体状态的东西都是protected,但 getter 可以public(或不是)。这使得遍历聚合之间的关系以获取所需的数据实际上非常安全(实体看起来就像来自其包外部的值对象)。
  2. 由于上述原因,修改聚合状态的应用程序服务必须与聚合位于同一包内。
  3. 您的存储库可能需要进行一些转换,但这应该非常安全。
  4. 所有跨聚合边界的状态更改都是通过域事件完成的。
  5. 根据您设置 FK 的方式,如果您有要在多个聚合中运行的预删除域逻辑,从数据库中删除实体可能会有点棘手,但无论如何,在这样做之前您确实应该三思而后行。

就是这样。我确信这不是任何灵丹妙药,但这种模式已经对我有用了一段时间了。


小智 1

出于多种原因,缺乏解决方案可能是一件好事。通常,在我看来,域结构和持久性策略是分离的,这是相当理智的。您可能希望以独立的方式应用一些与设计域模型的方式相关的持久性模式。在从上到下进行设计时,您不关心处理遗留表,并且您的 jpa 实体可能与域实体有很大不同。这有什么问题吗?所以这不是问题,因为您继续使用类似于 FP 的方法在存储库中实现域/jpa 实体映射,减少 bolerplate 的事情并抛开 DAO 调用的副作用。