Skip to content

Criteria Queries and JPA Metamodel with Spring Boot and Kotlin

Cleaner, easier, more robust criteria queries in Kotlin

JPA criteria queries require us to reference entity classes and their attributes. The easiest way to do this is by using strings. This means we have to remember the names of the entity attributes when writing a query. But what happens if the name of an entity attribute changes? Well, since we referenced it with a string, we have to update it wherever it has been used. This can cause issues, however, when what should be easy refactoring becomes a tedious and error-prone activity.

Luckily for us, this can be avoided. JPA 2 defines a typesafe Criteria API which allows criteria queries to be constructed by using the so-called JPA Metamodel. This feature was introduced to avoid the aforementioned drawback and provide a type-safe and static way to access the metadata of the entity classes. Please note that the task of the metamodel generation can be automated. JBoss, EclipseLink, OpenJPA, DataNucleus are just some of the tools available for the metamodel generation.

In this article, we will see how one of these metamodel generator tools can be integrated into a Spring Boot and Kotlin project.

Canonical Metamodel

The metamodel is a set of objects that describe your domain model.
[…]
This metamodel is important in 2 ways. First, it allows providers and frameworks a generic way to deal with an application’s domain model.
[…] 
Second, from an application writer’s perspective, it allows very fluent expression of completely type-safe criteria queries. — Hibernate Community Documentation

The structure of the metamodel classes is described in the JPA 2 (JSR 317) specification. If you are interested in understanding in detail how metamodel classes are defined, I recommend reading this documentation page first. Otherwise, feel free to skip to the next chapter.

Configuring a Metamodel Generation Tool

To generate the metamodel classes, we will use hibernate-jpamodelgen, the metamodel generator tool provided by JBoss.

First of all, we need to add the hibernate-jpamodelgen dependency to our build.gradle.kts file, as well the kapt:

plugins {
   kotlin("kapt") version "1.3.72"
}
dependencies {
    implementation ("org.hibernate:hibernate-jpamodelgen:5.4.12.Final")
    
    kapt("org.hibernate:hibernate-jpamodelgen:5.4.12.Final")   
}

kapt is the Kotlin Annotation Processing Tool and it is required by hibernate-jpamodelgen to automatically generate the metamodel classes during build-time.

JPA Metamodel Classes

To show what a metamodel class looks like, we need an entity class. Since defining JPA entities in Kotlin can be tricky, I recommend reading this article first.

Let’s assume we have already defined the Author entity class as follows:

@Entity
open class Author {

    @get:Id
    @get:GeneratedValue
    @get:Column(name = "id")
    open var id: Int? = null

    @get:Column(name = "name")
    open var name: String? = null

    @get:Column(name = "surname")
    open var surname: String? = null

    @get:Column(name = "birth_date")
    @get:Temporal(TemporalType.DATE)
    open var birthDate: Date? = null

    @get:ManyToOne(fetch = FetchType.LAZY)
    @get:JoinColumn(name = "country_id")
    open var country: Country? = null

    @get:ManyToMany(fetch = FetchType.LAZY)
    @get:JoinTable(
            name = "author_book",
            joinColumns = [JoinColumn(name = "author_id")],
            inverseJoinColumns = [JoinColumn(name = "book_id")]
    )
    open var books: MutableSet<Book> = HashSet()
}

By default, the corresponding metamodel class will be placed by kapt in build/generated/source/kapt/ followed by the same package as the corresponding entity class.

The metamodel class will be automatically generated during build-time and will have the same name as the entity with an added “_” at the end. So, the metamodel class generated for the Author class will be Author_ and look like this:

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Author.class)
public abstract class Author_ {

	public static volatile SingularAttribute<Author, Integer> id;
	public static volatile SingularAttribute<Author, String> name;
	public static volatile SingularAttribute<Author, String> surname;
	public static volatile SingularAttribute<Author, Date> birthDate;
	public static volatile SingularAttribute<Author, Country> country;
	public static volatile SetAttribute<Author, Book> books;

	public static final String ID = "id";
	public static final String NAME = "name";
	public static final String SURNAME = "surname";
	public static final String BIRTH_DATE = "birthDate";
	public static final String COUNTRY = "country";
	public static final String BOOKS = "books";
}

As you can tell, the generated metamodel class is a Java class. This is not a problem since Kotlin is fully interoperable with Java.

JPA Metamodel In Action

Since the Criteria API provides overloaded methods that accept String references as well as Attribute interface implementations, we can use the generated metamodel classes in the same way we would use the String references to attributes. Let’s write the criteria query that will fetch all authors named “John”.

This is what the criteria query using Author_ looks like:

// entityManager setup code ...

val criteriaBuilder = entityManager.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Author::class.java)

// retrieving all authors named "John"
val root = criteriaQuery.from(Author::class.java)
criteriaQuery
    .select(root)
    .where(
        criteriaBuilder.equal(root.get(Author_.name), "John")
    )
val query = entityManager.createQuery(criteriaQuery)

val resultList = query.resultList

And this is what it looks like without using the metamodel class:

// entityManager setup code ...

val criteriaBuilder = entityManager.criteriaBuilder
val criteriaQuery = criteriaBuilder.createQuery(Author::class.java)

// retrieving all authors named "John"
val root = criteriaQuery.from(Author::class.java)
criteriaQuery
    .select(root)
    .where(
        // why <String> is required -> https://stackoverflow.com/a/59831558
        criteriaBuilder.equal(root.get<String>("name"), "John")
    )
val query = entityManager.createQuery(criteriaQuery)

val resultList = query.resultList

The main difference lies in how the entity attribute name is retrieved. In the first snippet, we used the Author_.name reference instead of the conventional column name. If that attribute changes, in the first example we will get a compile-time error, while in the second one, a more dangerous runtime error will be thrown. Using metamodel classes makes the code cleaner, easier, and more robust.

Conclusion

JPA Metamodel provides a type-safe way to define criteria queries. This makes future refactorings far easier than referencing the attributes via strings, causing code to be more robust to changes.

Many different metamodel generator tools can be used with an annotation processor to generate the metamodel classes at build time. This means that changes in entity attributes will automatically be reflected in the metamodel classes, avoiding runtime errors.

Thanks for reading! I hope that you found this article helpful.

nv-author-image

Antonello Zanini

I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.View Author posts

Want technical content like this in your blog?