本文要点
- JPA 2.2 在去年夏天发布,交付了一些备受期待的功能改善:
- 对 Java 8 特性的更好支持,例如 Date 和 Time API
- 新增对 AttributeConverter 的 CDI 支持
- 一些注解变得 @Repeatable
- 你现在可以获取 java.util.Stream 形式的查询结果
- 准备针对 Java 9 的规范
去年夏天发布, JPA 2.2 是 Java EE 8 中比较稳健的规范之一,可能是因为 JPA 2.1 已经是一个非常成熟的规范,提供了现代应用程序所需的大部分功能。
尽管如此,JPA 2.2 带来了一些显著的变更。首先,它引入了 2 个小规模的变更:
- 增加对 AttributeConverter 的 CDI 支持
- 准备针对 Java 9 的规范
其次,也更重要的是,JPA 2.2 规范支持 Java 8 的一些特性:
- JPA 2.x 当前版本支持 Date 和 Time API 的一些类(并没有支持所有的类,下面会详细介绍)
- 一些注解变得可重复
- 你现在可以获取 java.util.Stream 形式的的查询结果
我们稍后会介绍更多关于这些内容的细节。
一些背景信息:Java EE 7(包含 JPA 2.1)在 Java 8 之前发布,因此不能对 Java 8 引入的任何数据类型和编程概念提供支持。因此,JPA 2.2 及其对 Java 8 的支持备受期待。
在你的项目中加入 JPA 2.2
像所有 Java EE 规范一样,JPA 只定义 API,允许开发者在不同的实现中选择适合自己的。
下面的 Maven 坐标会增加 JPA 2.2 API 到你的项目中。
<dependency> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> <version>2.2</version> </dependency>
尽管 JPA 2.1 有一些不同的实现,但是只有 EclipseLink 提供了完整的 JPA 2.2 实现。你可以使用下面的 Maven 坐标将它增加到你的项目中。
<dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.7.0</version> </dependency>
在 AttributeConverter 中的 CDI 注入
AttributeConverter 为读取和写入自定义数据类型提供了一种易用的标准化方法。它通常用在 JPA 2.1 中,来增加对 LocalDate 和 LocalDateTime 的支持,或者来实现一个自定义的枚举映射。
实现一个 converter 非常简单;只需要实现 AttributeConverter 接口,为这个类添加 @Converter 注解,然后实现 2 个接口方法:
- convertToDatabaseColumn——将 Java 类转变为它的数据库表现形式
- convertToEntityAttribute——相反地,将数据库列转变回 Java 对象
实现一个自定义枚举映射
下面的代码片段展示了一个简单的例子:
@Converter(autoApply = true) public class ContactInfoTypeConverter implements AttributeConverter<ContactInfoType, String> { @Override public String convertToDatabaseColumn(ContactInfoType type) { switch (type) { case PRIVATE: return "P"; case BUSINESS: return "B"; default: throw new IllegalArgumentException("ContactInfoType [" + type.name() + "] not supported."); } } @Override public ContactInfoType convertToEntityAttribute(String dbType) { switch (dbType) { case "P": return ContactInfoType.PRIVATE; case "B": return ContactInfoType.BUSINESS; default: throw new IllegalArgumentException("ContactInfoType [" + dbType + "] not supported."); } } }
AttributeConverter 将我们的 ContactInfoType 枚举转变为一个 String。现在你可能会疑惑,为什么你要实现自己的转换器,因为 JPA 已经提供了 2 种可选方案来持久化一个枚举:1)String 表现形式;2)使用特定枚举值的序号值。但是如果你想改变枚举的时候,就会发现这两种方案都各有它们的缺点。当将一个枚举持久化为一个 String 时,你如果想要修改任何枚举值的名字,就需要更改你的数据库。另一方面,当将枚举持久化为序号值时,因为序号值代表了枚举值在枚举定义中的位置,因此,如果你更改了枚举值的顺序,或者你增加了新的枚举值并且不是放在最后,或者你删掉了除最后一个枚举值之外的其它任何枚举值,你就又需要更新你的持久化数据了。
因此,在上述任何一种策略中,如果你使用 JPA 的标准映射,你的枚举的大部分变更都会需要数据库更新。否则,你的持久层框架会无法映射已存在的值,或者会把它们映射成错误的枚举值。
你可以通过使用 AttributeConverter 实现一个自定义的映射来避免上述缺点,这样你就可以控制映射了,并且在需要重构枚举时,避免任何对已存在的映射的变更。
使用 CDI 注入
正如你在先前的代码片段中看到的那样,我用 AttributeConverter 实现了映射。如果你不需要复用这个转换实现,那么它是一个不错的方案。但是,如果你想要复用它,JPA 2.2 允许你选择使用 CDI 注入来将你的转换实现注入到你的 AttributeConverter 中。
@Converter(autoApply = true) public class ContactInfoTypeCdiConverter implements AttributeConverter<ContactInfoType, String> { // Conversion implementation class: @Inject ContactInfoTypeHelper conversionHelper; @Override public String convertToDatabaseColumn(ContactInfoType type) { return conversionHelper.convertToString(type); } @Override public ContactInfoType convertToEntityAttribute(String dbType) { return conversionHelper.convertToContactInfoType(dbType); } }
使用 Date 和 Time 类作为实体属性
Java 8 的 Date 和 Time API 备受期待,许多开发者想要使用这些新类作为实体属性。不幸的是,JPA 2.1 在 Java 8 之前发布,因此不支持这些类。
在 JPA 2.2 发布之前,你有两种方法来持久化 dates 和 times:
- 例如,实现一个 AttributeConverter,将一个 LocalDate 映射为一个 java.sql.Date 。Spring Data 团队和许多其他开发者都是这样做的。
- 使用持久层框架的一些特性,例如 Hibernate 对 Date 和 Time API 的支持。
JPA 2.2 规范现在支持 Date 和 Time API 的一些类作为基础类型,因此你不再需要提供任何额外的映射注解,例如用于 java.util.Date 的 @Temporal 注解。相比于过去的 java.util.Date,Date 和 Time API 中的类将简单的日期和带时间的日期区别开来。因此,持久层框架拥有将这些类持久化成相应的 SQL 数据类型而需要的所有信息。
@Entity public class DateAndTimeEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; private LocalDate date; private LocalDateTime dateTime; public LocalDate getDate() { return date; } public void setDate(LocalDate date) { this.date = date; } public LocalDateTime getDateTime() { return dateTime; } public void setDateTime(LocalDateTime dateTime) { this.dateTime = dateTime; } public Long getId() { return id; } }
尽管 JPA 2.2 未支持全部 Date 和 Time API,但是一些实现,例如 Hibernate,会提供更多类的专门支持,例如 java.time.Duration。如果可以在下一个发布版本中加入这些功能会非常棒,但是因为 Oracle 正将所有的规范移交给 Eclipse 基金会,因此我们需要拭目以待。
Java Type
JDBC Type
java.time.LocalDate
DATE
java.time.LocalTime
TIME
java.time.LocalDateTime
TIMESTAMP
java.time.OffsetTime
TIME_WITH_TIMEZONE
java.time.OffsetDateTime
TIMESTAMP_WITH_TIMEZONE
一些 JPA 注解变得可重复
可重复的注解允许你用相同的注解多次注解类、方法或属性。那意味着你不再需要用像 @NamedQueries 来包裹多个 @NamedQuery 那样来使用注解。对比过去的方式:
@NamedQueries({ @NamedQuery(name = EntityWithNamedQueries.findByName, query = "SELECT e FROM EntityWithNamedQueries e WHERE e.name = :name"), @NamedQuery(name = EntityWithNamedQueries.findByContent, query="SELECT e FROM EntityWithNamedQueries e WHERE e.content = :content") }) public class EntityWithNamedQueries { … }
对于使用多个注解的实体,使用新的注解方式提升了代码的可读性。
@NamedQuery(name = EntityWithMultipleNamedQuery.findByName, query = "SELECT e FROM EntityWithNamedQueries e WHERE e.name = :name") @NamedQuery(name = EntityWithMultipleNamedQuery.findByContent, query = "SELECT e FROM EntityWithNamedQueries e WHERE e.content = :content") public class EntityWithMultipleNamedQuery { … }
在 JPA 2.2 中,可以被包裹进一个容器注解中的所有注解都变得可重复。这些注解有:
- AssociationOverride
- AttributeOverride
- JoinColumn
- MapKeyJoinColumn
- NamedEntityGraph
- NamedNativeQuery
- NamedQuery
- NamedStoredProcedureQuery
- PersistenceContext
- PersistenceUnit
- PrimaryKeyJoinColumn
- SecondaryTable
- SqlResultSetMapping
- SequenceGenerator
- TableGenerator
获得 Stream 形式的查询结果
Stream API 是 Java 8 引入的另外一个流行的特性,而且许多开发者想要使用它来处理他们的查询结果。
JPA 2.1 支持的唯一方式是调用 Query 接口的 getResultList 方法,来获取 List 形式的查询结果。然后,你可以调用 List 接口的 stream 方法来获取它的 Stream 形式:
TypedQuery<DateAndTimeEntity> q = em.createQuery( "SELECT e FROM DateAndTimeEntity e", DateAndTimeEntity.class); Stream<DateAndTimeEntity> s = q.getResultList().stream(); {1} {1}
JPA 2.2 引入了一种更直接的方式来获取 Stream 形式的查询结果。你现在可以简单地调用 Query 接口的 getResultStream 方法来获取 Stream 查询结果。
TypedQuery<DateAndTimeEntity> q = em.createQuery( "SELECT e FROM DateAndTimeEntity e", DateAndTimeEntity.class); Stream<DateAndTimeEntity> s = q.getResultStream(); {1} {1}
过去的方案就很有效,你可能会疑问为什么 JPA 2.2 为此引入一种新的方法。之所以这么做,是有 2 个原因:
- 新的 getResultStream 方法比之前的方案更直接。
- 它使得持久层框架可以不同地实现这两种方法。
第 2 个原因尤为重要;当你调用 getResultStream 方法,你的持久层框架必须从数据中获取完整的结果集,然后将它存储在本地内存中。无论是你想要将结果作为 List 处理,或者是将它发送给客户端应用程序,都没有问题。但是如果你在使用 Stream,这可能不是一个可选的方案。当你处理 Stream 的时候,你遍历它的元素并且一个接一个处理这些元素,因此,你不需要在开始处理它们之前获取所有的元素。当你需要的时候再小批次的加载它们,稍后再释放它们,会让你更高效地使用资源。JDBC 为这些用例提供了动态化的结果集。getResultStream 方法使得持久层框架可以使用这种方案,而不是一次获取所有记录。
但是,在你在项目中使用这个方法之前,你应该了解 2 件事:
首先,JPA 规范提供的 Query 接口包含了一个默认方法。这个方法直接调用 getResultList 方法,然后将 List 结果转变为 Stream。因此,如果持久层框架没有重写这个方法,它只是提供了一点易用性的改善。但是,所有常用的 JPA 实现应该都会重写这个方法。例如,Hibernate 已经在它的 Query 接口上提供了一个实现了相同功能的结果方法。如果他们没有复用这个实现来重写JPA 的getResultStream 方法,我才会感到惊讶。
其次,你应该始终牢记,有些事情用SQL 查询做比使用Stream API 更高效。从Stream 中过滤一些元素,在某个节点取消处理过程,或者改变元素的顺序,可能比较有吸引力。但是,用SQL 语句来做这些事情是一种更好的实践。数据库对这些类型的操作做了高度优化,而且比你用Java 实现的任何方法都更高效。最重要的是,你通常很可能会减少需要从数据库传送到应用程序的数据量,将它们映射成实体,然后存储在内存中。因此,始终确保在查询中执行尽可能多的操作,并尽可能充分地利用SQL 的功能。
只要你遵循这些简单的规则,新的getResultStream 方法提供了一种获取和处理Stream 形式的查询结果的不错方式。
总结
JPA 2.2 是一次维护性的小发布,并没有引入太多变更。但是,它确实交付了一些经常被请求的改善,特别是对 Java 8 特性的支持,例如对 Date 和 Time API 的支持,以及获取 Stream 形式的查询结果。
总之,JPA 2.2 规范提供了一组重点关注开发者常用需求的稳定功能集。这对于当前 JPA 和所有其它 Java EE 规范向 Eclipse 基金会的迁移来说,是一个不错的情况。
关于作者
Thorben Janssen 是一名独立的培训师和顾问,也是 Amazon 畅销书《Hibernate Tips - More than 70 solutions to common Hibernate problems》的作者。他已经有15 年Java 和Java EE 开发经验,是CDI 2.0 专家组(JSR 365)成员。他有自己的博客—— www.thoughts-on-java.org ,在上面写一些 Java EE 相关话题的文章。
查看英文原文: JPA 2.2 Brings Some Highly Anticipated Changes
感谢冬雨对本文的审校。
评论