spring - 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)
}
解决方案
我为这个问题找到的解决方案是具有抽象域实体,由我的类在持久层实现(可能是也可能不是 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.
}
需要注意的几点:
- 任何修改实体状态的都是
protected
,但 getter 可以是public
(或不是)。这使得遍历聚合之间的关系以获取您需要的数据实际上非常安全(实体看起来就像来自其包外部的值对象)。 - 由于上述原因,修改聚合状态的应用程序服务必须与聚合位于同一包内。
- 您的存储库可能需要进行一些转换,但它应该非常安全。
- 跨聚合边界的所有状态更改都是通过域事件完成的。
- 根据您设置 FK 的方式,如果您有预删除域逻辑以在多个聚合中运行,则从数据库中删除实体可能会有点棘手,但无论如何在这样做之前您确实应该三思而后行。
而已。我确信它不是任何类型的灵丹妙药,但这种模式已经为我服务了一段时间了。
推荐阅读
- mysql - 连接表是否会增加不必要的复杂性?
- vue.js - 使用 then/Catch 异步获取 VuexFire 数据
- c# - 如何在 WPF 应用程序的 RichTextBox 中记录时间?
- python - 为什么 sigmoid 函数的输出层获取值 0 或 1 {0,1} 而不是在 [0,1] 中获取值
- c - 这些类型的声明在 C 中有多常见?
- python - 用于合并 DXF 文件的 Python 模块
- sequelize.js - Sequelize - 具有两个嵌套包含的嵌套列语法
- c++ - 成员数组的 C++ countof 实现
- python - 显示列表中的信息时,有没有办法四舍五入到小数点后两位?
- sql-server - 将base64转换为可读字符串