Java Text Blocks – Using Multiline Strings with Hibernate & JPA
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
JPA and Hibernate require you to write lots of Strings. You use them to create ad-hoc and named queries with JPQL, to define native SQL queries and to specify the fetching behavior in an EntityGraph. Until Java 13, the lack of multiline Strings in Java made all of these tasks either annoying to implement or the code hard to read. You either had to concatenate multiple Strings or put everything into a single-line String.
String sql = "SELECT new com.thorben.janssen.BookAuthorReviewCount(b.title, concat(a.firstName, ' ', a.lastName), size(b.reviews)) FROM Book b JOIN b.author a GROUP BY b.title, a.firstName, a.lastName"
That changed drastically with the introduction of Java Text Blocks. You can finally define Strings that span over multiple lines by using 3 quotation marks.
String sql = """ SELECT new com.thorben.janssen.BookAuthorReviewCount( b.title, concat(a.firstName, ' ', a.lastName), size(b.reviews) ) FROM Book b JOIN b.author a GROUP BY b.title, a.firstName, a.lastName """
As you can see, that made the SQL statement much easier to read. And because this is a standard Java feature, you can use it everywhere you use a String. But it’s especially useful if the String is long and complex enough to add linebreaks. Let’s take a look at a few examples.
Text blocks in JPQL and HQL queries
JPA’s query language JPQL and the Hibernate-specific extension HQL enable you to write queries based on your entity model. The syntax is very similar to SQL, and I explained it in great detail in my guide to JPQL.
JPQL and HQL are not as powerful as SQL. Nonetheless, you can create pretty complex queries that are hard to read in a single line.
The following query returns BookAuthorReviewCount objects that might be used in a list view in the UI. The query selects the name the author by joining firstName and lastName, the title of the Book, and counts the number of Reviews of each Book.
This query isn’t easy to read if you write it as a simple, single line String.
TypedQuery<BookAuthorReviewCount> q = em.createQuery( "SELECT new com.thorben.janssen.BookAuthorReviewCount(b.title, concat(a.firstName, ' ', a.lastName), size(b.reviews)) FROM Book b JOIN b.author a GROUP BY b.title, a.firstName, a.lastName", BookAuthorReviewCount.class); List<BookAuthorReviewCount> books = q.getResultList();
Adding a few linebreaks and handling it as a multiline String makes that much easier.
TypedQuery<BookAuthorReviewCount> q = em.createQuery(""" SELECT new com.thorben.janssen.BookAuthorReviewCount( b.title, concat(a.firstName, ' ', a.lastName), size(b.reviews) ) FROM Book b JOIN b.author a GROUP BY b.title, a.firstName, a.lastName """, BookAuthorReviewCount.class); List<BookAuthorReviewCount> books = q.getResultList();
And the same is true if you create the same query as a named query. You can then use the text block within the @NamedQuery annotation.
@Entity @NamedQuery( name = "selectBookAuthorReviewCount", query = """ SELECT new com.thorben.janssen.BookAuthorReviewCount( b.title, concat(a.firstName, ' ', a.lastName), size(b.reviews) ) FROM Book b JOIN b.author a GROUP BY b.title, a.firstName, a.lastName """) public class Author { ... }
Text blocks in native SQL queries
JPA was intentionally designed as a leaky abstraction that enables you to access the underlying JDBC layer. You can use it to write and execute native SQL queries that your persistence provider doesn’t analyze. Using this approach, you can use all query features supported by your database.
I use that in the following SQL statement to select the title of all blog posts and books, the type of publication, and the name of the author. As you can see, books and blog posts are stored in 2 separate tables. I query both tables to get the title together with the type of publication and use a UNION clause to merge the results into one result set.
Query q = em.createNativeQuery(""" SELECT title, 'blog' as type, firstName, lastName FROM blogpost JOIN author on author.id = blogpost.author_id UNION SELECT title, 'book' as type, firstName, lastName FROM book JOIN author on author.id = book.author_id """, "PublicationAuthorMapping"); List<PublicationAuthor> pubs = q.getResultList();
That’s something you can’t do with JPQL. But you can easily do it using a native SQL statement. And if you combine your native SQL query with an @SqlResultSetMapping, you can get your query result as entity objects, DTO objects, or scalar values.
I referenced such a mapping in the previous code snippet to map each record in the result set to a PublicationAuthor object. The required mapping definition is relatively simple. You only need to use a @ConstructorResult annotation, provide the class you want to instantiate as the targetClass, and define an array of @ColumnResult annotations to specify the constructor parameters.
@Entity @SqlResultSetMapping( name = "PublicationAuthorMapping", classes = @ConstructorResult( targetClass = PublicationAuthor.class, columns = {@ColumnResult(name = "title"), @ColumnResult(name = "type"), @ColumnResult(name = "firstName"), @ColumnResult(name = "lastName")})) public class Author { ... }
Text blocks to define EntityGraphs
You can not only use Java text blocks to define your queries. In version 5.4, Hibernate introduced an API to parse a String into an EntityGraph. These Strings describe a hierarchical structure and their readability benefits from multiline Strings.
An EntityGraph tells Hibernate which associations it shall initialize when fetching the result of a query. This is an important performance tuning tool that you need to know when working with Hibernate.
The String used in the following example gets parsed into an EntityGraph that tells Hibernate to fetch the book and blogPost associations defined on the Author entity. For the book association, it will also fetch the associated publisher and the editor who worked on the book.
RootGraph graph = GraphParser.parse(Author.class, """ blogPosts, books(publisher(editor))""", em); TypedQuery<Author> q = em.createQuery("SELECT a FROM Author a", Author.class); q.setHint(GraphSemantic.FETCH.getJpaHintName(), graph); List<Author> authors = q.getResultList();
If you’re using such an EntityGraph, Java’s text block feature can improve the readability of your code. But you also need to doublecheck your query and analyze if fetching that many associations makes your query too complex. Depending on the number of elements in each association, it might be better to split this query into multiple ones.
Conclusion
Java’s text blocks might look like a small feature, but they can improve the readability of your code a lot.
Because it’s a standard Java feature, you can use it everywhere in your code. But not all places will benefit the same. Text blocks are especially useful if the created String naturally contains linebreaks or gets easier to read if you split it over multiple lines.
With JPA and Hibernate, you create a lot of Strings that belong in the 2nd category. Especially queries get often long and complex. Spreading them over multiple lines allows you to structure them visually and to improve their readability. I’m sure that it won’t take long until everyone uses Java text blocks to write their queries.