使用JPA实现领域驱动设计的实用指南
领域驱动设计(DDD)是一种强调领域及其逻辑重要性的软件开发方法。然而,将其传播到数据库实体设计并不常见且相当棘手。本文探讨了使用Java持久化API(JPA)实现DDD的原则和策略,为开发人员提供实用见解。
代码库背景
在以下章节中,我们将项目视为单体应用,但拆分为多个模块(Maven模块),每个模块处理后端的一个子域。它们之间不存在依赖关系,至少没有实体相关的依赖,以遵循与DDD配套的单一职责原则(SRP)。因此,想法是让每个子域的数据库实体与其他子域实体共存在一个数据库模式中。因此,实体将以某种方式重叠,但不会完全纠缠在一起。为了与DDD保持一致并维护适当的领域边界和职责,关键是将它们视为数据库表的某些方面。
核心原则
主要原则是应用单一职责原则(SRP)进行数据写入:只有一个子域负责写入数据库列。其他子域要么不考虑该列,要么仅读取它。
第二个原则是允许读取重叠:多个域可以以只读模式访问同一列。
如您所见,这里不需要应用复制。即使这仍然是可能的,目标是管理一个规范化形式的数据库。
开始前的最佳实践
一个高度建议的做法是将实体图设计为树:实体只能通过聚合根访问;从任何“侧面”访问实体是被禁止的。这样做的好处包括:
- 主要好处是它通常帮助您的ORM处理持久化,因为您可以负担得起将所有内容设置为急切加载(避免N+1问题),并且它还有助于生成使用连接的更好的SQL查询。
- 它澄清了代码,并让您将域名与聚合根对齐。
另一个重要建议是掌握您的ORM:
- 学习继承映射的不同形式(通常的、多态的)及其对数据库设计的影响。
- 掌握其注解,例如,在连接表时(您可以调整默认的连接列)或使用
@Embeddable
或@SecondaryTable
组合实体。 - 选择合适的标识符策略(注意某些JPA策略在数据库上的结果不同,取决于自动增量、序列等的支持),特别是对于只读实体;已分配的标识符是合适的。
一些技巧
如上所述,一个非常有用的技巧是将某些属性设置为只读。为此,JPA允许将列标记为“insertable false”和“updatable false”。因此,为了避免列被写入,只需像下面的示例一样用两者标记它:
|
|
在处理关系时,我们通过跳过级联应用相同的方法:
|
|
一个通常已知但值得为我们目标提及的技巧是,您可以将实体和列名与它们的数据库表镜像解耦。这非常重要,因为领域驱动设计试图与业务领域词汇相匹配。因此,您可能在子域之间存在一些细微差异,就像同一枚硬币的两面。
最后一个技巧由于允许许多组合,需要许多示例,因此我们仅枚举并提醒一些在定义实体结构时可能非常有用的注解的存在:
@Embeddable
是一个简单的注解,对于我们的情况,可以将一些具有特定概念的列推到一边,或使主实体更轻量,而原始概念在另一个子域中更完整,并且整体上是实体的完全一部分。@SecondaryTable
在某些读取情况下可以替换@Inheritance(JOINED)
,取决于您必须处理的设计或复杂性。@Inheritance(SINGLE_TABLE)
允许将同一表中的不同概念分离到清晰的层次结构中;然而,您必须小心此类注解,因为它需要填充可能不是您实体一部分的强制列。
不幸的是,我们无法在这里探索所有结构化注解及其可能性,因为它们的数量以及潜在组合太多。然而,我希望给您一些想法,并提供一些对JPA注解的新视角。
陷阱
如开头所述,如果项目是单体的并由子模块组成,您可能在其上有一个聚合器,如果我们考虑Java作为本文的主要目标,这通常是Spring Boot。那么您应该意识到这种方法的一些缺点:
- 由于JPA禁止在同一持久化上下文中两次使用相同的实体名称,您将需要使用
@Entity(name="...")
来命名它们以克服名称冲突。您可以简单地在实体名称前加上子域名称以避免“重复”。 - 如果您更喜欢拥有多个
@PersistenceContext
(每个子域一个),您将需要命名它们并在您的服务中注入正确的那个。此外,您必须让它扫描正确的子包;否则,它将迭代整个类路径并生成一个包含所有子域实体的巨大持久化上下文。 - 最后,测试和只读属性的问题来了。如果您隔离您的域并且彼此不可见,您将难以轻松插入数据,仅仅因为您的JPA上下文不允许填充只读列。那么您有两个选择:要么使用一些低级访问(JDBC、脚本等)来完成,要么……创建另一个专用于数据库填充的实体模型用于测试。
结论
在开发JPA实体时遵守领域驱动设计(DDD)和单一职责原则(SRP)的原则,是创建可维护代码的宝贵且稳健的策略。在本文中,我们学习了几种在代码库中应用这些原则的技术;然而,它们需要对JPA有深刻的理解。此外,在集成使用此方法的各种子模块时,必须注意并警惕可能出现的潜在陷阱。
同时,我的经验表明,从长远来看,这种投资是值得的,因为它使我们能够在不诉诸微服务的情况下解耦不同的子域实体。这种技术可以成为CRUD后端的强大挑战者。