Extend Hibernate to Handle Java Stream Queries

by Per Minborg

on September 24, 2020

The Java Stream API released in Java 8 has proven itself to be an efficient, terse yet intuitive way of expressing application logic. The newly launched open-source project JPAstreamer allows you to express Hibernate or other JPA database queries using Java Streams. In this article, we will show you how to extend the API of your existing database applications to handle Stream queries in an instant. 

To quickly give you an idea of what JPAstreamer accomplish, we’ll start by showing you an example of a Stream operating on a database table that contains arbitrary users (with attributes including a first and last name): 

jpaStreamer.stream(User.class
.filter(User$.firstName.startsWith(”A”)
.sort(User$.lastName.reversed())
.limit(10)
.forEach(System.out::println);

This will print ten users with a first name that starts with the letter A sorted in reversed order based on their last names. Omitting the details (that are covered shortly), this demonstrates how the desired result set is easily described as a pipeline of Stream operators. 

On the surface, it may look as if the presented Stream would require every row in the User-table to be materialized in the JVM. Although, the Stream is actually optimized and rendered to JPA queries. Thus, the Stream queries are as performant as alternative approaches i.e. JPQL or Criteria Builder, but with the difference that JPAstreamer constitutes a streamlined and type-safe approach to expressing queries.

How JPAstreamer Works

JPAstreamer plugs into your application with the addition of a single dependency in your Maven/Gradle build. The specific dependency is described here

Like the well-known Java library Lombok, JPAstreamer uses an annotation processor to form a meta-model at compile time. It inspects any classes marked with the standard JPA annotation @Entity and for every entity Foo.class, a corresponding Foo$.class is generated. The generated classes represent entity attributes as Fields that are used to form predicates on the form User$.firstName.startsWith(”A”) that can be interpreted by JPAstreamer’s query optimizer.  

It is important to note that JPAstreamer does not alter or disturb the existing codebase, but simply extends the API to handle Java Stream queries from this point forward. Further, the meta-model is placed in “generated sources” located in the “target” folder and doesn’t need to be checked in with the source code nor tested.

Let’s Get Streaming 

We’ll now walk you through the easy process of setting up JPAstreamer in your database application. To follow along, your application must use Java 8 (or later) and Hibernate or another JPA provider that is responsible for object persistence (if you wish to use the Stream API without JPA, you are better off using the open-source Stream ORM Speedment). 

As mentioned, installation simply entails adding a dependency (described here) to your Maven/Gradle build and rebuilding the application to generate the JPAstreamer meta-model. 

Once you’ve completed the simple set-up, you need to obtain an instance of JPAStreamer like so: 

JPAStreamer jpaStreamer = JPAStreamer.of("db-name"); 

You should replace the String ”db-name” with the name of the persistence unit you wish to query. Look it up in your JPA configuration-file (often named persistence.xml) under the tag <persistence-unit>.

The JPAstreamer instance provides access to the method .stream() that accepts the name of the Entity you wish to Stream. To query the user table, you would simply type: 

jpaStreamer.stream(User.class); 

This returns a stream of all the user rows of type Stream<User>. With a Stream source at hand, you are free to add any Java Stream operations to form a pipeline through which the data will flow (data flowing is a conceptual image rather than an actual description of how the code executes). For example: 

List users = jpaStreamer.stream(User.class)
.filter(User$.age.greaterThan(20))
.map(u -> u.getFirstName() + ” ” + u.getLastName())
.collect(Collectors.toList);

This Stream collects the name of users who reached the age of 20 in a List. User$ refers to the generated entity that is part of JPAstreamer’s meta-model. This entity is used to form predicates and comparators for operations such as .filter() and .sort() which are quickly composed leveraging code completion in modern IDEs. 

Here is another example that counts all the users who are from Germany and are named “Otto” using a combined predicate: 

long count = jpaStreamer.stream(User.class)
.filter(User$.country.equal(”Germany”) .and(User$.firstName.equal(”Otto”))
.count();

Conclusion

In this article, we have shown how you can integrate the open-source library JPAstreamer with Hibernate (or any JPA provider) to compose type-safe and expressive database queries as standard Java Streams. 

Resources

Author 

Per Minborg
Julia Gustafsson

About

Per Minborg

Per Minborg is a Palo Alto based developer and architect, currently serving as CTO at Speedment, Inc. He is a regular speaker at various conferences e.g. JavaOne, DevNexus, Jdays, JUGs and Meetups. Per has 15+ US patent applications and invention disclosures. He is a JavaOne alumni and co-author of the publication “Modern Java”.