Since version 5.2, Hibernate starts to use Java 8 classes in their proprietary APIs. The JPA standard will most likely do the same with version 2.2, but who knows when we will get that. Until then, Hibernate’s proprietary APIs are all we have, and I will present the most interesting changes here on the blog.
In one of my previous posts, I wrote about Hibernate’s support for the DateTime API. In this post, I want to show you a small addition to the Query interface. The new stream() method allows you to process the query results as a Java 8 Stream.
But before we dive into the details, let me quickly explain the benefits of the new stream() method.
Advantages of the stream() method
In the beginning, it looks like a small improvement that makes your code a little less clunky. You already can take the List of query results and call its stream() method to get a Stream representation.
List<Book> books = session.createQuery("SELECT b FROM Book b", Book.class).list(); books.stream() .map(b -> b.getTitle() + " was published on " + b.getPublishingDate()) .forEach(m -> log.info(m));
Sure, this code also gives you a Stream with your query result, but it is not the most efficient approach. In this example, Hibernate will get all the selected Book entities from the database, store them in memory and put them into a List. You then call the stream() method and process the results one by one. That approach is OK as long as your result set isn’t too big. But if you’re working on a huge result set, you better scroll through the result records and fetch them in smaller chunks. You’re already familiar with that approach if you’ve used JDBC result sets or Hibernate’s ScrollableResult. Scrolling through the records of a result set and processing them as a Stream are a great fit. Both approaches process one record after the other, and there’s no need to fetch all of them upfront. The Hibernate team, therefore, decided to reuse the existing scroll() method and the ScrollableResult to implement the new stream() method.
How to use the stream() method
As you might have guessed from the introduction, the change on the API level is quite small. It’s just the stream() method on the Query interface. But sometimes that’s all it takes to adapt an existing API so that you can use it in a modern way. The stream() method is part of the Query interface and you can, therefore, use it with all kinds of queries and projections. Let’s have a look at a few of them.
Entities are the most common projection with Hibernate, and you can use them in a Stream in any way you like. In the following example, I call the stream() method to get the result set as a Stream. I then use the map() method to create a log message for each Book entity and call the forEach() method to write each message to the log file. Your business logic will probably be a little more complex.
Stream<Book> books = session.createQuery("SELECT b FROM Book b", Book.class).stream(); books.map(b -> b.getTitle() + " was published on " + b.getPublishingDate()) .forEach(m -> log.info(m));
Up to now, scalar values were not a very popular projection because it returns a List of Object. You then have to implement a loop to go through all Objects and cast its elements to their specific types. That gets a lot easier with Streams. The following code snippet shows a simple native SQL query which returns 2 scalar values.
Stream<Object> books = session.createNativeQuery("SELECT b.title, b.publishingDate FROM book b").stream(); books.map(b -> new BookValue((String)b, (Date)b)) .map(b -> b.getTitle() + " was published on " + b.getPublishingDate()) .forEach(m -> log.info(m));
If you still don’t want to write this kind of code, have a look at my posts about @SqlResultMapping. You can do the same with a set of annotations
POJOs or similar projections can be easily created with a constructor expression, as you can see in the following code snippet. Unfortunately, there seems to be a bug (HHH-11029) in Hibernate 5.2.2 so that these projections don’t work with Streams. Instead of mapping the BookValues to Strings and writing them to the log file, the following code snippet throws a ClassCastException.
Stream<BookValue> books = session.createQuery("SELECT new org.thoughts.on.java.model.BookValue(b.title, b.publishingDate) FROM Book b", BookValue.class).stream(); books.map(b -> b.getTitle() + " was published on " + b.getPublishingDate()) .forEach(m -> log.info(m));
How to NOT use the stream() method
The stream() method provides a comfortable and efficient way to get a Stream representation of your query result. I already talked about the advantages of this new method and how you can use it. But one important thing I haven’t talked about is how to NOT use it. It is not directly related to the stream() method. It’s related to a lot of Stream API examples I’ve seen since the release Java 8.
The Stream API provides a set of methods that make it easy to filter elements, to check if they match certain criteria and to aggregate all elements. In general, these methods are great but please don’t use them to post-process your query results. As long as you can express these operations with SQL (believe me, you can implement almost all of them in SQL), the database can do them a lot better!
Summary and cheat sheet
Hibernate 5.2 introduced the stream() method to the Query interface. It seems like just a small change, but it provides easy access to a Stream representation of the result set and allows you to use the existing APIs in a modern way.
As you’ve seen in the examples, the stream() method can be used with all kinds of queries and almost all projections. The only projection that’s causing some problems in version 5.2.2 is the projection as a POJO. But I expect that to be fixed soon.