Spring Boot:如何从 JPA/Hibernate 注释中保持 DDD 实体的清洁?
Posted
技术标签:
【中文标题】Spring Boot:如何从 JPA/Hibernate 注释中保持 DDD 实体的清洁?【英文标题】:Spring Boot: How to keep DDD entities clean from JPA/Hibernate Annotations? 【发布时间】:2020-01-20 22:22:52 【问题描述】:我正在编写一个我希望遵循 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)
【问题讨论】:
您可以查看 JDX ORM for Java。 JDX 对域模型(实体)类是非侵入性的。映射是在外部以基于简单语法的声明方式定义的。没有注释会污染您的代码。没有 XML 复杂性。免责声明:我是 JDX ORM 的架构师。 你找到解决办法了吗?我有完全一样的问题。我正在考虑创建一个单独的域类和单独的数据类。我认为这在我们遇到的情况下是有道理的。检查此链接:***.com/questions/14024912/…。但是,即使我们创建了 2 个模型(域和数据模型),我仍然无法意识到如何正确连接它们。 @SpasojePetronijević 不幸的是,我还没有找到满意的答案。实际上,我正在考虑通过研究 Entity Framework 和 NHibernate 中的代码来自己编写这样一个库。另一种可能性是创建一个可以自动生成 XML 映射的类。令人沮丧的是,像 .NET ORM 中具有流畅 API 的映射配置类会好得多,但我们在 Spring 和 Hibernate 中没有。他们在 JVM 世界中只有注解和 XML,它的 2019 已经和 lambda 表达式从 Java 8 开始可用,来吧... @LordYggdrasill 有什么消息吗?有些人说他们不应该使用 DTO,因为它很冗长...... @BraianSilva 不幸的是,除了使用 DTO 和编写大量样板代码或处理域实体中的注释之外,我还没有找到解决问题的方法。有一天我会写一个类似于Entity Framework的fluent mapping的库,但是在那之前…… 【参考方案1】:我为这个问题找到的解决方案是使用 抽象域实体,由我的类在持久层实现(可能是也可能不是 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 的方式,如果您有要在多个聚合中运行的预删除域逻辑,从数据库中删除实体可能会有些棘手,但在这样做之前您确实应该三思而后行。李>
就是这样。我敢肯定这不是什么灵丹妙药,但这种模式对我来说已经有一段时间了。
【讨论】:
这很有趣。但是,您如何处理域实体的创建?也许在底层的工厂?如果您必须从父域中创建子域实体(即论坛实体创建讨论实体),则需要将工厂注入实体。 我找到的另一种方法是域逻辑和状态之间的分离。所以域实体有一个状态,是db实体实现的接口。 @OfirWinegarten 是的,我使用在域层定义并在基础设施层实现的工厂。关于创建子实体,您可以只在父域类中使用一个抽象方法来处理实例化。具体实现可以负责处理 Spring 并设置适当的引用。此外,状态模式当然也适用于此。我只是对一直委派 getter 感到恼火。 好的。最后一个问题——如果所有实体都是抽象的,你如何测试你的域?你如何实例化它们?莫基托?存根? 你有很多选择,但我通常创建测试子类,它们从所有抽象方法返回适合域的范围内的随机值,然后使用 Mockito 来满足我对每个测试更具体的需要。不是每个人都喜欢随机测试,但我发现它们非常有用。【参考方案2】:由于几个原因,缺乏解决方案可能是件好事。 通常,在我看来,域结构和持久性策略是分离的。 您可能希望根据设计域模型的方式以独立的方式应用一些持久性模式。 在从上到下进行设计时,您不关心处理遗留表,并且您可以拥有与域实体完全不同的 jpa 实体。那有什么问题呢? 所以这不是问题,因为您一直在使用类似 FP 的方法在您的 repo 中实现域/jpa 实体映射,减少了 bolerplate 事情并搁置了 DAO(s) 调用的副作用。
【讨论】:
我不知道你用类似 FP 的方法实现域/jpa 实体映射是什么意思。我没有用 ORM 做 FP,而且我也不认为它是一种优越的技术。以上是关于Spring Boot:如何从 JPA/Hibernate 注释中保持 DDD 实体的清洁?的主要内容,如果未能解决你的问题,请参考以下文章
如何从 spring-boot-starter-parent 中排除特定依赖项
如何从 Spring Boot 应用程序属性加载 Spring config xml $ 值
如何从另一个 Spring Boot 应用程序访问一个 Spring Boot 应用程序的内存 H2 数据库
如何从 Spring Boot 项目创建 jar,这个 jar 我们想在另一个 Spring Boot 应用程序中使用?