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.