本文将了解基于 Hibernate 的 Java 持久化标准,学习如何使用 JPA 在关系数据库或 NoSQL 数据库中存储和管理 Java 对象。
作为一种规范,Jakarta Persistence API(以前称为 Java Persistence API)更关注持久性,这大概意味着 Java 对象比创建它们的应用程序进程存活更久。
并非所有 Java 对象都需要持久化,但大多数应用程序都会持久化关键业务对象。JPA 规范允许您定义应保留哪些对象,以及它们在 Java 应用程序中的保留方式。
JPA 本身不是工具或框架;相反,它定义了一组指导实施者的概念。虽然 JPA 的对象关系映射 (ORM) 模型最初是基于 Hibernate,但后来有所发展。同样,虽然 JPA 最初旨在与关系数据库一起使用,但一些 JPA 实现已扩展为与 NoSQL 数据存储一起使用。支持 JPA 和 NoSQL 的流行框架是 EclipseLink,它是 JPA 3 的参考实现。
与 JDBC 不同,JPA 背后的核心思想是,在大多数情况下,JPA避免需要让您“从关系上思考”。在 JPA 中,您在 Java 代码和对象领域定义持久性规则,而 JDBC 需要您手动将代码转换为关系表,然后再转换回来。
JPA 3 在 Jakarta EE
Java Persistence API 最初是作为 Java EE 5 中的 Enterprise JavaBeans 3.0 规范 (JSR 220) 的子集发布的。从 Java EE 6 (JSR 317) 中的 JPA 2.0 发布开始,它已经发展成为自己的规范。JPA 于 2019 年被采纳为 Jakarta EE 的独立项目。
- 截至撰写本文时,当前版本为 JPA 3.1。
流行的 JPA 实现,如 Hibernate 和 EclipseLink 现在支持 JPA 3。从 JPA 2 迁移到 JPA 3 涉及到一些名称空间更改,但除此之外,这些更改是底层性能提升。
JPA 和 Hibernate
由于它们相互交织的历史,Hibernate 和 JPA 经常被混为一谈。但是,与 Java Servlet 规范一样,JPA 催生了许多兼容的工具和框架。Hibernate 只是众多 JPA 工具中的一种。
Hibernate 由 Gavin King 开发并于 2002 年初首次发布,是一个用于 Java 的 ORM 库。King 开发了 Hibernate 作为实体 bean 的替代品以实现持久性。该框架非常流行,当时非常需要,以至于它的许多想法都被采纳并编入了第一个 JPA 规范。
今天,Hibernate ORM 是最成熟的 JPA 实现之一,并且仍然是 Java 中 ORM 的流行选择。
- 撰写本文时的最新版本 Hibernate ORM 6 实现了 JPA 2.2。
其他 Hibernate 工具包括 Hibernate Search、Hibernate Validator 和 Hibernate OGM,后者支持 NoSQL 的域模型持久性。
JPA 和 EJB
如前所述,JPA 是作为 Enterprise JavaBeans (EJB) 3.0 的一个子集引入的,但后来发展成为它自己的规范。EJB 是一种与 JPA 侧重点不同的规范,它是在 EJB 容器中实现的。每个 EJB 容器都包含一个由 JPA 规范定义的持久层。
什么是Java ORM?
虽然它们在执行上有所不同,但每个 JPA 实现都提供某种 ORM 层。为了理解 JPA 和 JPA 兼容工具,您需要很好地掌握 ORM。
对象关系映射是一项任务——开发人员有充分的理由避免手动执行。像 Hibernate ORM 或 EclipseLink 这样的框架将该任务编入一个库或框架,一个 ORM 层。作为应用程序架构的一部分,ORM 层负责管理软件对象与关系数据库中的表和列交互的转换。在 Java 中,ORM 层将 Java 类和对象进行转换,以便它们可以在关系数据库中存储和管理。
默认情况下,被持久化的对象的名称成为表的名称,字段成为列。设置好表格后,每个表格行对应于应用程序中的一个对象。对象映射是可配置的,但默认值往往有效,并且通过坚持使用默认值,您可以避免维护配置元数据。
JPA 与 NoSQL
直到最近,非关系数据库才引起人们的好奇。NoSQL 运动改变了这一切,现在 Java 开发人员可以使用各种 NoSQL 数据库。一些 JPA 实现已经发展到包含 NoSQL,包括 Hibernate OGM 和 EclipseLink。
图1
图 1 说明了 JPA 和 ORM 层在应用程序开发中的作用。
配置 Java ORM 层
当您设置新项目以使用 JPA 时,您将需要配置数据存储和 JPA 提供程序。您将配置一个数据存储连接器以连接到您选择的数据库(SQL 或 NoSQL)。您还将包含并配置 JPA 提供程序,它是一个框架,例如 Hibernate 或 EclipseLink。虽然您可以手动配置 JPA,但许多开发人员选择使用 Spring 的开箱即用支持。我们将很快查看手动和基于 Spring 的 JPA 安装和设置。
Java 数据对象
Java 数据对象 (JDO) 是一种标准化的持久性框架,它与 JPA 的主要区别在于支持对象中的持久性逻辑,以及它对使用非关系数据存储的长期支持。JPA 和 JDO 非常相似,JDO 提供者通常也支持 JPA。请参阅 Apache JDO 项目以了解有关 JDO 与其他持久性标准(如 JPA 和 JDBC)的关系的更多信息。
Java 中的数据持久化
从编程的角度来看,ORM 层是一个适配器层:它使对象图的语言适应 SQL 和关系表的语言。ORM 层允许面向对象的开发人员构建能够持久保存数据的软件,而无需离开面向对象的范例。
使用 JPA 时,您创建了一个从数据存储到应用程序数据模型对象的映射。您无需定义对象的保存和检索方式,而是定义对象与数据库之间的映射,然后调用 JPA 来持久化它们。如果您使用的是关系数据库,那么您的应用程序代码和数据库之间的大部分实际连接将由 JDBC 处理。
作为规范,JPA 提供了元数据注释,您可以使用它来定义对象和数据库之间的映射。每个 JPA 实现都为 JPA 注释提供了自己的引擎。JPA 规范还提供了 PersistanceManager 或 EntityManager,它们是与 JPA 系统的关键联系点(其中您的业务逻辑代码告诉系统如何处理映射的对象)。
为了使所有这些更加具体,请考虑清单 1,这是一个用于为音乐家建模的简单数据类。
清单 1.Java 中的一个简单数据类
清单 1 中的 Musician 类用于保存数据。它可以包含原始数据,例如名称字段。它还可以保持与其他类的关系,例如 mainInstrument 和 performances。
音乐家存在的理由是包含数据。这种类型的类有时称为 DTO,即数据传输对象。DTO 是软件开发的一个共同特征。虽然它们包含多种数据,但它们不包含任何业务逻辑。持久化数据对象是软件开发中普遍存在的挑战。
使用 JDBC 的数据持久化
将 Musician 类的实例保存到关系数据库的一种方法是使用 JDBC 库。JDBC 是一个抽象层,它允许应用程序在不考虑底层数据库实现的情况下发出 SQL 命令。
清单 2 展示了如何使用 JDBC 持久化 Musician 类。
清单 2. JDBC 插入一条记录。
清单 2 中的代码是相当自我记录的。georgeHarrison 对象可以来自任何地方(前端提交、外部服务等),并设置了其 ID 和名称字段。对象上的字段然后用于提供 SQL 插入语句的值。(PreparedStatement 类是 JDBC 的一部分,提供了一种安全地将值应用于 SQL 查询的方法。)
虽然 JDBC 提供了手动配置附带的控件,但与 JPA 相比,它很麻烦。要修改数据库,您首先需要创建一个从 Java 对象映射到关系数据库中的表的 SQL 查询。然后,只要对象签名发生更改,您就必须修改 SQL。使用 JDBC,维护 SQL 本身就成为一项任务。
使用 JPA 的数据持久化
现在考虑清单 3,我们在其中使用 JPA 持久化 Musician 类。
清单 3. 使用 JPA 持久化 George Harrison。
清单 3 用一行 entityManager.save() 替换了清单 2 中的手动 SQL,它指示 JPA 保留对象。从那时起,该框架将处理 SQL 转换,因此您永远不必离开面向对象的范例。
JPA 中的元数据注释
清单 3 中的魔法是配置的结果,它是使用 JPA 的注释创建的。开发人员使用注释来通知 JPA 哪些对象应该被持久化,以及它们应该如何被持久化。
清单 4 显示了带有单个 JPA 注释的 Musician 类。
清单 4. JPA 的 @Entity 注释
持久对象有时称为实体。将 @Entity 附加到像 Musician 这样的类会通知 JPA 该类及其对象应该保留。
XML 与基于注解的配置
JPA 还允许您使用外部 XML 文件来定义类元数据,而不是注释。但你为什么要这样对自己?
配置 JPA
与大多数现代框架一样,JPA 采用约定编码(也称为约定优于配置),其中框架提供基于行业最佳实践的默认配置。例如,默认情况下,名为 Musician 的类将映射到名为 Musician 的数据库表。
常规配置可以节省时间,并且在许多情况下效果很好。也可以自定义 JPA 配置。例如,您可以使用 JPA 的 @Table 注释来指定应该存储 Musician 类的表。
清单 5. JPA 的 @Table 注释
清单 5 告诉 JPA 将实体(Musician 类)持久保存到 Musician 表。
主键
在 JPA 中,主键是用来唯一标识数据库中每个对象的字段。主键对于引用对象和将对象关联到其他实体很有用。每当您将对象存储在表中时,您还将指定要用作其主键的字段。
在清单 6 中,我们告诉 JPA 使用哪个字段作为 Musician 的主键。
清单 6. 指定主键:
在本例中,我们使用 JPA 的 @Id 注释将 id 字段指定为 Musician 的主键。默认情况下,此配置假定主键将由数据库设置——例如,当字段设置为表上的自动递增时。
JPA 支持用于生成对象主键的其他策略。它还具有用于更改单个字段名称的注释。通常,JPA 足够灵活以适应您可能需要的任何持久性映射。
增删改查操作
一旦您将一个类映射到数据库表并建立了它的主键,您就拥有了在数据库中创建、检索、删除和更新该类所需的一切。调用 entityManager.save() 将创建或更新指定的类,具体取决于主键字段是 null 还是应用于现有实体。调用 entityManager.remove() 将删除指定的类。
实体关系
简单地持久化具有原始字段的对象只是等式的一半。JPA 还允许您管理相互关联的实体。表和对象中可能存在四种实体关系:
- 一对多
- 多对一
- 多对多
- 一对一
每种类型的关系都描述了一个实体如何与其他实体相关。例如,Musician 实体可能与 Performance 具有一对多关系,Performance 是一个由集合(如 List 或 Set)表示的实体。
如果 Musician 包含 Band 字段,则这些实体之间的关系可能是多对一的,这意味着单个 Band 类上的 Musician 集合。(假设每个音乐家只在一个乐队中表演。)
如果 Musician 包含 BandMates 字段,则它可以表示与其他 Musician 实体的多对多关系。
最后,Musician 可能与 Quote 实体存在一对一关系,用于表示名言:Quote famousQuote = new Quote()。
定义关系类型
JPA 对其每个关系映射类型都有注释。清单 7 展示了如何注释 Musician 和 Performances 之间的一对多关系。
清单 7. 注释一对多关系
需要注意的一件事是 @JoinColumn 告诉 JPA Performance 表上的哪一列将映射到 Musician 实体。
每场表演都将与一位音乐家相关联,该音乐家由此列进行跟踪。当 JPA 将 Musician 或 Performance 加载到数据库中时,它将使用此信息来重构对象图。
实体状态和分离的实体
实体是对象的总称,其持久性与 ORM 映射。正在运行的应用程序中的实体将始终处于四种状态之一:瞬态、托管、分离和删除。
您将在 JPA 中遇到的一种情况是分离的实体。这仅仅意味着您正在处理的对象已经脱离了支持它们的数据存储中的内容,并且支持它们的会话已经关闭。换句话说,JPA 希望使对象保持最新状态,但它做不到。您可以通过在实体上调用 entityManager.merge() 来重新附加分离的实体。
任何不持久的对象都是瞬态的。该对象此时只是一个潜在的实体。一旦调用了 entityManager.persist() ,它就变成了一个持久实体。
托管对象是一个持久实体。
当一个实体已从数据存储中删除,但仍作为活动对象存在时,我们称其处于已删除状态。
Vlad Mihalcea 写了一篇关于实体状态的精彩讨论,以及 JPA 的 EntityManager 和 Hibernate 的 Session 类之间用于管理它们的细微差别。
EntityManager.flush() 有什么用?
许多刚接触 JPA 的开发人员想知道 EntityManager.flush() 方法的用途。JPA 管理器将缓存保持实体的持久状态与数据库一致所需的操作,并对它们进行批处理以提高效率。
但有时,您需要手动使 JPA 框架执行将实体推送到数据库所需的操作。例如,这可能会导致执行数据库触发器。在这种情况下,您可以使用 flush() 方法,所有尚未持久化的实体状态将立即发送到数据库。
获取策略
除了知道将相关实体放在数据库中的什么位置之外,JPA 还需要知道您希望如何加载它们。获取策略告诉 JPA 如何加载相关实体。加载和保存对象时,JPA 框架必须提供微调对象图处理方式的能力。例如,如果 Musician 类有一个 bandMate 字段(如清单 7 所示),加载 george 可能会导致从数据库中加载整个 Musician 表!
您需要能够定义相关实体的延迟加载——当然要认识到 JPA 中的关系可以是急切的或延迟的。您可以使用注释来自定义您的抓取策略,但 JPA 的默认配置通常开箱即用,无需更改:
一对多:惰性
多对一:Eager
多对多:惰性
一对一:Eager
JPA 中的事务
虽然不在本简短介绍的范围内,但事务允许开发人员写入数据库。在清单 1 中,我们使用以下行实现了一个简单的事务:em.getTransaction().commit();。事务可以通过多种方式定义,从通过 API 的显式交互,到使用注释来定义事务边界,再到使用 Spring AOP 来定义事务。
JPA 安装和设置
最后,我们将快速浏览一下为您的 Java 应用程序安装和设置 JPA。对于这个演示,我将使用 JPA 参考实现 EclipseLink。
安装 JPA 的常用方法是将 JPA 提供程序包含到您的项目中。清单 8 展示了如何将 EclipseLink 作为依赖项包含在 Maven pom.xml 文件中。
清单 8. 将 EclipseLink 作为 Maven 依赖项包含在内:
您还需要包含数据库的驱动程序,如清单 9 所示。
清单 9. MySql 连接器的 Maven 依赖项:
接下来,您需要将您的数据库和提供商告知系统。这是在 persistence.xml 文件中完成的,如清单 10 所示。
清单 10.Persistence.xml:
还有其他方法可以向系统提供此信息,包括以编程方式。我建议使用 persistence.xml 文件,因为以这种方式存储依赖项可以很容易地更新您的应用程序而无需修改代码。
JPA 的 Spring 配置
使用 Spring 将极大地简化 JPA 到您的应用程序的集成。例如,在您的应用程序标头中放置 @SpringBootApplication 注释会指示 Spring 根据您指定的配置自动扫描类并根据需要注入 EntityManager。
清单 11 显示了如果您希望 Spring 的 JPA 支持您的应用程序需要包含的依赖项。
清单 11. 在 Maven 中添加 Spring JPA 支持:
何时使用 JPA
在设计 Java 应用程序时,是否使用 JPA 的问题是分析瘫痪的常见来源。尤其是在尝试做出前期技术决策时,您不希望数据持久性(一个重要的长期因素)出错。
要打破这种瘫痪状态,记住应用程序可以演变为使用 JPA 是很有用的。您可以使用 JDBC 或 NoSQL 库构建探索性代码或原型代码,然后开始添加 JPA。这些解决方案没有理由不能共存。
在因优柔寡断而瘫痪之后,下一个最糟糕的事情就是采用 JPA,因为它意味着额外的努力将阻止项目向前推进。JPA 可以成为整体系统稳定性和可维护性的胜利,而这些对于更成熟的项目来说是极好的目标;然而,有时越简单越好,尤其是在项目开始时。
如果您的团队没有能力预先采用 JPA,请考虑将其列入未来的路线图。
结论
每个处理数据库的应用程序都应该定义一个应用程序层,其唯一目的是隔离持久性代码。正如您在本文中所见,Jakarta Persistence API 引入了一系列功能并支持 Java 对象持久性。简单的应用程序可能不需要 JPA 的所有功能,并且在某些情况下配置框架的开销可能不值得。然而,随着应用程序的增长,JPA 的结构和封装确实得到了保留。使用 JPA 可以使您的目标代码保持简单,并为访问 Java 应用程序中的数据提供一个常规框架。
了解有关 Jakarta Persistence API 和相关技术的更多信息:
- 什么是 JDBC?Java 数据库连接简介:使用 JDBC 连接到数据库、处理 SQL 查询等的概述和指南。
- 使用 JPA 和 Hibernate 的 Java 持久性,第 1 部分:建模实体和关系。
- 使用 JPA 和 Hibernate 的 Java 持久性,第 2 部分:多对多关系和继承关系。
- 如果你的下一个项目使用 JPA?:可以帮助您做出决定的关键问题。
这个故事“什么是 JPA?Java 持久性简介”最初由 JavaWorld 发布。