BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

JPA 2.2带来一些备受期待的变更

| 作者 Thorben Janssen 关注 0 他的粉丝 ,译者 张健欣 关注 0 他的粉丝 发布于 2018年2月7日. 估计阅读时间: 23 分钟 | QCon北京2018全面起航:开启与Netflix、微软、ThoughtWorks等公司的技术创新之路!

亲爱的读者:我们最近添加了一些个人消息定制功能,您只需选择感兴趣的技术主题,即可获取重要资讯的邮件和网页通知

本文要点

  • 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:

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();

JPA 2.2 引入了一种更直接的方式来获取Stream形式的查询结果。你现在可以简单地调用Query接口的getResultStream方法来获取Stream查询结果。

TypedQuery<DateAndTimeEntity> q = em.createQuery(
    "SELECT e FROM DateAndTimeEntity e", DateAndTimeEntity.class);
Stream<DateAndTimeEntity> s = q.getResultStream();

过去的方案就很有效,你可能会疑问为什么JPA 2.2为此引入一种新的方法。之所以这么做,是有2个原因:

  1. 新的getResultStream方法比之前的方案更直接。
  2. 它使得持久层框架可以不同地实现这两种方法。

第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

感谢冬雨对本文的审校。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT