首页 > 解决方案 > Spring Boot:如何从 JPA/Hibernate 注释中保持 DDD 实体的清洁?

问题描述

我正在编写一个希望遵循 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;
}

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

我希望域实体是 POJO(或我使用 Kotlin 时的 POKO),所以我不想在实体类上有这样的注释。但是我绝对不希望使用 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)
}

标签: springhibernatespring-bootormdomain-driven-design

解决方案


我为这个问题找到的解决方案是具有抽象域实体,由我的类在持久层实现(可能是也可能不是 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.

然后你的 Person 类可能看起来像这样:

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.
    }

}

您的 FullName 类可能如下所示:

public final class FullName {

    private final String firstName;
    private final String lastName;

    // Constructors, factories, getters...

}

然后,最后,您的 JpaPerson 类应如下所示:

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

    @Column(name="firstName") 
    private String firstName;

    @Column(name="lastName") 
    private String lastName;

    @OneToMany(cascade=ALL, mappedBy="item")
    private Set<Item> items;

    @Override
    public int getId() { return id; }

    @Override
    public FullName getName() { return FullName.of(firstName, lastName); }

    @Override
    protected void setName(FullName name) {
       firstName = name.getFirst();
       lastName = name.getLast();
    }

    // Implementations for the rest of the abstract methods...
    // Notice the complete absence of "business stuff" around here.    
}

需要注意的几点:

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

而已。我确信它不是任何类型的灵丹妙药,但这种模式已经为我服务了一段时间了。


推荐阅读