Key Takeaways
- JPA 2.2 was released in the summer, delivering some frequently requested enhancements:
- Better alignment with Java 8 features like the Date and Time API
- Added CDI support to AttributeConverter
- Several annotations became @Repeatable
- You can now retrieve a query result as a java.util.Stream
- Prepared the specification for Java 9.
Released this past summer, JPA 2.2 is one of more modest specifications in Java EE 8, perhaps because JPA 2.1 was already a very mature specification, providing most of the features required by modern applications.
JPA 2.2 nevertheless did bring several notable changes; First it introduced two small-scale changes:
- added CDI support to AttributeConverter
- prepared the specification for Java 9.
And second (and more importantly) it aligned the specification with Java 8:
- JPA 2.now supports some (but not all, as well will see) of the classes of the Date and Time API
- several annotations became repeatable, and
- you can now retrieve a query result as a java.util.Stream.
We will talk more about all of these later.
Some background; Java EE 7, which included JPA 2.1, was released before Java 8, and so couldn’t provide any support for the data types and programming concepts introduced in Java 8. So JPA 2.2 and its Java 8 support was highly anticipated.
Adding JPA 2.2 to Your Project
Like all Java EE specifications, JPA only defines the API, allowing the developer to choose between different implementations.
The following Maven coordinates add the JPA 2.2 API jar to your project.
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>javax.persistence-api</artifactId>
<version>2.2</version>
</dependency>
Although there are several implementations of JPA 2.1, only EclipseLink provides a complete JPA 2.2 implementation. You can use the following Maven coordinates to add it to your project.
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.7.0</version>
</dependency>
CDI Injection in AttributeConverter
An AttributeConverter provides an easy to use standardized method for reading and writing custom data types. It was often used in JPA 2.1 to add support for LocalDate and LocalDateTime, or to implement a custom enum mapping.
The implementation of a converter is pretty simple; just implement the AttributeConverter interface, annotate the class with the @Converter annotation, and implement the two interface methods:
- convertToDatabaseColumn - converts the Java class to its database representation
- convertToEntityAttribute - conversely, converts the column back to an object.
Implement a custom enum mapping
You can see a simple example in the following code snippet.
@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.");
}
}
}
This AttributeConverter converts our ContactInfoType enum into a String. Now you may be wondering, why would you implement your own conversion, given that JPA already supports two options to persist an enum: 1) the String representation or 2) the ordinal value of the particular enum value. Both options have their disadvantages if you need to change the enum. If you persist an enum as a String, you need to update your database if you decide to change the name of any enum value. On the other hand, if you persist the ordinal value, which represents the position of the value in the enum definition, then if you change the order of the enum values, or if you add new values anywhere but as the last entry, or if you remove any of the enum values besides the last one, you will again need to update your persisted data.
So in either strategy, if you use JPA’s standard mapping most changes to your enum will also require a database update. Otherwise your persistence provider will not be able to map the existing values, or will map them to the wrong enum value.
You can avoid that by implementing a custom mapping with an AttributeConverter, which gives you control of the mapping and allows you to avoid any changes to the existing mappings if you need to refactor your enum.
Use CDI Injection
As you’ve seen in the previous code snippet, I implemented the mapping within the AttributeConverter. That’s a good approach in cases where you are not reusing the conversion implementation. If, however you are aiming for reuse, JPA 2.2 offers the option to use CDI injection to inject your conversion implementation classes into your 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 and Time Classes as Entity Attributes
Java 8’s Date and Time API was highly anticipated, and many developers wanted to use these new classes as entity attributes. Unfortunately, JPA 2.1 was released before Java 8 and so didn’t support these classes.
Before the release of JPA 2.2, you had two options for persisting dates and times:
- Implement an AttributeConverter, which would map a LocalDate to a java.sql.Date, for example. That’s what the Spring Data team and a lot of other developers did.
- Use proprietary features of your persistence provider, like Hibernate’s support for the Date and Time API.
With JPA 2.2 the specification now supports some of the classes of the Date and Time API as basic types, so you no longer need to provide any additional mapping annotations, like the @Temporal annotation that was used with a java.util.Date. In contrast to the old java.util.Date, the classes of the Date and Time API distinguish between a simple date and a date with time. So, the persistence provider has all the required information to map these classes to a matching SQL data type.
@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;
}
}
Although JPA 2.2 does not support the full Date and Time API, some implementations, like Hibernate, provide proprietary support for more classes, like java.time.Duration. It would be great if these could be added to the specification in the next release, but since Oracle is handing over all specs to the Eclipse foundation, we need to wait and see.
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 |
Some JPA annotations become @Repeatable
Repeatable annotations allow you to annotate a class, method or attribute multiple times with the same annotation. That means you no longer need to use annotations like @NamedQueries to wrap multiple @NamedQuery annotations. Contrast the old style:
@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 { … }
with the new, which for entities that use several annotations, improves the readability of your code.
@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 { … }
With JPA 2.2, all annotations that can be wrapped into a container annotation, became repeatable. These are:
- AssociationOverride
- AttributeOverride
- JoinColumn
- MapKeyJoinColumn
- NamedEntityGraph
- NamedNativeQuery
- NamedQuery
- NamedStoredProcedureQuery
- PersistenceContext
- PersistenceUnit
- PrimaryKeyJoinColumn
- SecondaryTable
- SqlResultSetMapping
- SequenceGenerator
- TableGenerator
Get your query results as a Stream
The Stream API was another popular feature introduced in Java 8, and a lot of developers wanted to use it to process their query results.
The only option supported by JPA 2.1 was to call the getResultList
method on the Query
interface to retrieve the query result as a List
. You could then call the stream method on the List
interface to get a Stream
representation of it:
TypedQuery<DateAndTimeEntity> q = em.createQuery(
"SELECT e FROM DateAndTimeEntity e", DateAndTimeEntity.class);
Stream<DateAndTimeEntity> s = q.getResultList().stream();
JPA 2.2 introduced a more direct option to get the query result as a Stream. You can now simply call the getResultStream method on the Query interface to retrieve the Stream.
TypedQuery<DateAndTimeEntity> q = em.createQuery(
"SELECT e FROM DateAndTimeEntity e", DateAndTimeEntity.class);
Stream<DateAndTimeEntity> s = q.getResultStream();
The old approach obviously works and you might wonder why JPA 2.2 introduced a new method for it. There are two reasons:
- The new
getResultStream
method is more direct than the previous approach. - It enables a persistence provider to implement both methods differently.
The second reason is the more important one; when you call the getResultStream
method, your persistence provider has to fetch the complete result set from the database and store it in local memory. That might be ok, if you want to process the result as a List
or if you send it to a client application. But it might not be the optimal approach if you’re using a Stream
. When you process the Stream
, you iterate through its elements and process them one by one, so, you don’t need to fetch all elements before you start processing them. Loading them in small batches when you need them and releasing them afterwards would allow you to use your resources more efficiently. JDBC offers scrollable result sets for these use cases. The getResultStream
method enables the persistence provider to use that approach instead of fetching all records at once.
But before you use this method in your project, you should be aware of two things:
First, the Query
interface provided by the JPA specification contains a default method that just calls the getResultList
method and then transforms the List
into a Stream
. So, if the persistence provider doesn’t override this method, it just provides a small usability enhancement. But it’s to be expected that all commonly used JPA implementations will override it. Hibernate, for example, already provides the result method on its proprietary Query
interface that implements the same functionality. I would be surprised if they don’t reuse that implementation to override JPA’s getResultStream method.
Second, you should always keep in mind that it’s more efficient to do something in the SQL query instead of using the Stream API. It might be tempting to filter elements from the Stream
, to cancel the processing at some point or to change the order of the elements. But it is a better practice to do that within your SQL statement. Databases are highly optimized for these kinds of operations and do that more efficiently than any routines you implement in Java. On top of that, you will most likely reduce the amount of data you need to transfer from the database to your application, map to entities and store in memory. So, always make sure to perform as many operations as possible in your query and to use SQL’s capabilities to their fullest extend.
As long as you follow these simple rules, the new getResultStream
method provides a good way to retrieve and process your query result as a Stream
.
Summary
JPA 2.2 was a small maintenance release and didn’t introduce a lot of changes. But it did deliver some frequently requested enhancements, especially in providing better alignment with Java 8 features, like the support for the Date and Time API and the retrieval of a query result as a Stream
.
Overall, the specification provides a stable feature set that addresses the common needs of application developers. That’s a good situation for the current migration of JPA and all other Java EE specifications to the Eclipse Foundation.
Thorben Janssen is an independent trainer and consultant and the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems. He has been working with Java and Java EE for more than 15 years and is a member of the CDI 2.0 expert group (JSR 365). He writes about Java EE related topics on his blog www.thoughts-on-java.org.