DDD-五、使用 Spring Data 构建聚合
JPA 有自己的@Entity
概念,但远没有 DDD 中的实体概念那么严格。这既是优点也是缺点。优点是使用 JPA 实现实体和聚合非常容易。缺点是同样容易做 DDD 不允许的事情。如果您与以前广泛使用过 JPA 但不熟悉 DDD 的开发人员一起工作,这可能会特别成问题。
而值对象只是实现了一个空的标记接口,实体和聚合根将需要更广泛的基类。从一开始就正确设置基类很重要,因为以后更改它们将非常困难,尤其是当域模型变得很大时。为了帮助我们完成这项任务,我们将使用Spring Data。
Spring Data 提供了一些开箱即用的基类,您可以根据需要使用它们,因此让我们从查看它们开始。
使用Persistable
,AbstractPersistable
和AbstractAggregateRoot
Spring Data 提供了一个开箱即用的接口,称为Persistable
. 该接口有两种方法,一种用于获取实体的 ID,另一种用于检查实体是新的还是持久的。如果一个实体实现了这个接口,Spring Data 将使用它来决定在保存它时是调用persist
(新实体)还是merge
(持久化实体)。但是,您不需要实现此接口。Spring Data 还可以使用乐观锁版本来判断实体是否是新的:如果有版本,则持久化;如果没有,那就是新的。当您决定如何生成实体 ID 时,您需要注意这一点。
Persistable
Spring Data 还提供了实现接口的抽象基类: AbstractPersistable
. 它是一个泛型类,将 ID 的类型作为其单个泛型参数。ID 字段带有注释,@GeneratedValue
这意味着 Hibernate 等 JPA 实现将在实体首次持久化时尝试自动生成 ID。该类将具有非空 ID 的实体视为持久实体,将具有空 ID 的实体视为新实体。最后,它覆盖equals
andhashCode
以便在检查相等性时只考虑类和 ID。这与 DDD 一致 - 如果两个实体具有相同的 ID,则它们被视为相同。
如果您可以在实体 ID 中使用普通 Java 类型(例如Long
或UUID
)并让 JPA 实现在实体首次持久化时为您生成它们,那么这个基类是实体和聚合根的绝佳起点。但是等等,还有更多。
Spring Data 还提供了一个抽象基类,称为AbstractAggregateRoot
. 这是一个类 - 你猜对了 - 旨在通过聚合根进行扩展。但是,它*既不*扩展AbstractPersistable
也不实现Persistable
接口。那你为什么要使用这个类?好吧,它提供了允许聚合注册域事件的方法,然后在保存实体后发布这些事件。这真的很有用,我们将在以后的文章中回到这个主题。此外,不在基类中声明 ID 字段并让聚合根声明它们自己的 ID 也有一些好处。我们还将在稍后的帖子中回到这个主题。
在实践中,您希望聚合根是Persistable
,因此您最终实现了自己的基类的方法AbstractAggregteRoot
或AbstractPersistable
在您自己的基类中。接下来让我们看看如何做到这一点。
建立自己的基类
在我从事的几乎所有项目中,无论是在工作中还是在私人领域,我都是从创建自己的基类开始的。我的大部分领域模型都是从聚合根和值对象构建的;我很少使用所谓的本地实体(属于聚合但不是根的实体)。
我经常从一个名为的基类开始BaseEntity
,它看起来像这样:
@MappedSuperclass // <1>
public abstract class BaseEntity<Id extends Serializable> extends AbstractPersistable<Id> { // <2>
@Version // <3>
private Long version;
public @NotNull Optional<Long> getVersion() {
return Optional.ofNullable(version);
}
protected void setVersion(@Nullable Long version) { // <4>
this.version = version;
}
}
- 即使该类被命名
BaseEntity
,它也不是 JPA@Entity
而是@MappedSuperclass
. Serializable
界直接来自AbstractPersistable
。- 我对所有实体都使用乐观锁定。我们将在本文后面讨论这个问题。
- 在极少数情况下(如果有的话)需要手动设置乐观锁定版本。但是,为了安全起见,我提供了一种受保护的方法来实现这一点。我认为大多数拥有多年经验的 Java 开发人员都经历过这样的情况:他们真的需要在超类中设置属性或调用方法,却发现它是私有的。
一旦我的BaseEntity
课程到位,我就会继续学习BaseAggregateRoot
。这本质上是 Spring Data 的副本AbstractAggregateRoot
,但它扩展了BaseEntity
:
@MappedSuperclass // <1>
public abstract class BaseAggregateRoot<Id extends Serializable> extends BaseEntity<Id> {
private final @Transient List<Object> domainEvents = new ArrayList<>(); // <2>
protected void registerEvent(@NotNull Object event) { // <3>
domainEvents.add(Objects.requireNonNull(event));
}
@AfterDomainEventPublication // <4>
protected void clearDomainEvents() {
this.domainEvents.clear();
}
@DomainEvents // <5>
protected Collection<Object> domainEvents() {
return Collections.unmodifiableList(domainEvents);
}
}
- 这个基类也是一个
@MappedSuperclass
. - 此列表将包含我们要在保存聚合时发布的所有域事件。这是
@Transient
因为我们不想将它们存储在数据库中。 - 当您想从聚合中发布域事件时,您可以使用此受保护的方法注册它。我们将在本文后面仔细研究这一点。
- 这是一个 Spring Data 注释。Spring Data 将在域事件发布后调用此方法。
- 这也是一个 Spring Data 注释。Spring Data 将调用此方法来获取要发布的域事件。
就像我说的,我很少使用本地实体。然而,当这种需要出现时,我经常创建一个BaseLocalEntity
扩展BaseEntity
但不提供任何附加功能的类(除了,可能是对拥有它的聚合根的引用)。我将把这个作为练习留给读者。
乐观锁定
我们已经@Version
为乐观锁定添加了一个字段,BaseEntity
但我们还没有讨论原因。在战术领域驱动设计中,聚合设计的第四条准则是使用乐观锁定。但是为什么我们将@Version
字段添加到BaseEntity
而不是BaseAggregateRoot
呢?毕竟,负责维护聚合完整性的不是聚合根吗?
这个问题的答案是肯定的,但是在这里,底层的持久性技术(JPA 及其实现)再次潜入我们的领域设计。假设我们使用 Hibernate 作为我们的 JPA 实现。
Hibernate 不知道聚合根是什么——它只处理实体和嵌入。Hibernate 还跟踪实际更改了哪些实体,并且仅将这些更改刷新到数据库中。实际上,这意味着即使您明确要求 Hibernate 保存实体,实际上也不会向数据库写入任何更改,并且乐观版本号可能保持不变。
只要您只处理聚合根和值对象,这不是问题。对于 Hibernate,对可嵌入对象的更改始终是对其拥有实体的更改,因此实体的乐观版本(在本例中为聚合根)将按预期递增。但是,一旦您将本地实体添加到组合中,情况就会发生变化。例如:
@Entity
public class Invoice extends BaseAggregateRoot<InvoiceId> { // <1>
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private Set<InvoiceItem> items; // <2>
// The rest of the methods and fields are omitted
}
Invoice
是聚合根,因此它扩展了BaseAggregateRoot
类。InvoiceItem
是一个本地实体,因此它根据基类层次结构扩展BaseEntity
类或类。BaseLocalEntity
此类的实现并不重要,因此我们将其省略,但请注意注释中的级联选项@OneToMany
。
本地实体由其聚合根所有,因此通过级联进行持久化。但是,如果仅对本地实体而不是聚合根进行了更改,则保存聚合根只会导致本地实体被刷新到数据库中。在上面的示例中,如果我们仅对发票项目进行了更改,然后保存了整个发票,则发票版本号将保持不变。如果另一个用户在我们保存发票之前对同一项目进行了更改,我们将默默地用我们的更改覆盖其他用户的更改。
通过向 中添加乐观锁定版本字段BaseEntity
,我们可以防止出现这种情况。聚合根和本地实体都将被乐观锁定,并且不可能意外覆盖其他人的更改。
- 原文作者:知识铺
- 原文链接:https://geek.zshipu.com/post/DDD-%E4%BA%94%E4%BD%BF%E7%94%A8-Spring-Data-%E6%9E%84%E5%BB%BA%E8%81%9A%E5%90%88/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com