NashTech Blog

JPA Metamodel in Spring Boot: Type‑Safe Queries with Criteria API

Table of Contents

Introduce

If you’ve ever renamed an entity field and silently broke half your queries, you’ve met the limits of string-based paths in JPA. The Criteria API promises type safety, but without the right tooling you still end up writing brittle root.get(“title”) calls. JPA Metamodel fixes that by generating static, IDE-friendly classes (like Book_) so you can reference attributes type-safely, refactor with confidence, and catch mistakes at compile time.

In this guide, we’ll demystify the JPA Metamodel in the context of Spring Boot and Hibernate, showing how it streamlines dynamic queries—whether you’re hand-crafting Criteria queries or composing Spring Data JPA Specifications. You’ll see how the metamodel improves readability, reduces runtime errors, and pairs nicely with modern Spring Boot projects.

What is the JPA Metamodel?

The JPA Metamodel is a collection of classes that are automatically generated from your JPA entities. For each entity class (e.g., Book), a corresponding metamodel class (e.g., Book_) is created. This metamodel class contains static fields that represent the attributes of your original entity.

Let’s look at the “before” and “after”:

Before: The Stringly-Typed Approach

// Unsafe, no compile-time check, no IDE autocomplete for the string
Predicate authorPredicate = cb.equal(root.get("author"), "Tolkien");
Order publishDateOrder = cb.desc(root.get("publicationDate"));

After: The Type-Safe Metamodel Approach

// Safe, compile-time checked, and IDE-friendly!
Predicate authorPredicate = cb.equal(root.get(Book_.author), "Tolkien");
Order publishDateOrder = cb.desc(root.get(Book_.publicationDate));

Notice Book_.author and Book_.publicationDate? Those aren’t strings. They are static, typed fields from the generated Book_ class.

The key benefits are immediate:

  1. Type Safety: You are referencing an actual field, not just a string. The compiler knows the type (StringLocalDate, etc.), reducing the chance of type-mismatch errors.
  2. Compile-Time Checking: If you mistype Book_.auther instead of Book_.author, your code won’t even compile. The error is caught instantly, not at runtime.
  3. Refactoring Confidence: If you rename the author field in your Book entity using your IDE’s refactoring tools, it will automatically update all references, including Book_.author in your queries.
  4. Improved IDE Support: Your IDE will provide autocompletion for entity attributes, making query-building faster and less error-prone.

How it works

  • Annotation processing: During compilation, a processor (e.g., Hibernate’s hibernate-jpamodelgen) scans your @Entity and @Embeddable classes and generates “underscore” classes such as Book_ and Author_.
  • Static metamodel classes: Each generated class holds:
    • Typed attributes: SingularAttribute<Book, String>, SetAttribute<Book, String>, etc.
    • Optional String constants for property names (e.g., TITLE = “title”) for APIs that still need strings (like Sort).
  • Type-safe paths: In Criteria API and Specifications, you navigate attributes via these generated members, so renames or type changes break at compile time instead of production.
  • Standardized API: The types come from jakarta.persistence.metamodel.* (or javax.* on Boot 2), defined by the JPA spec.

Setting Up the JPA Metamodel with Spring Boot 3 and Maven

Spring Boot 3 uses the jakarta.* package as part of the move to Jakarta EE. This is an important detail when choosing the right dependency for our metamodel generator.

Step 1: Create a Spring Boot 3 Project

First, head over to start.spring.io and create a new project with the following dependencies:

  • Spring Web: For building web applications (optional, but good for testing).
  • Spring Data JPA: The core dependency for JPA.
  • H2 Database: An easy-to-use in-memory database for this example.
  • Lombok: Optional, but helps reduce boilerplate in our entity.

Step 2: Define a JPA Entity

Let’s create a simple Book entity in our project.

src/main/java/com/example/metamodel/Book.java

package com.example.metamodel;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDate;

@Entity
@Getter
@Setter
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String title;
    private String author;
    private LocalDate publicationDate;
    private String isbn;

    @ElementCollection
    private Set<String> tags;
}

This is a standard JPA entity. Now, let’s configure Maven to generate the metamodel for it.

Step 3: Configure Maven to Generate the Metamodel

This is the most crucial step. We need to tell Maven to use an annotation processor during the build process to scan our entities and generate the metamodel classes.

For Spring Boot 3 and Jakarta Persistence 3.1, the correct annotation processor is hibernate-jpamodelgen.

Open your pom.xml and add the following:

1. The hibernate-jpamodelgen dependency:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-jpamodelgen</artifactId>
    <!-- The version is managed by Spring Boot's parent POM -->
    <scope>provided</scope> 
</dependency>

Note: We use <scope>provided</scope> because the generator is only needed during compilation, not at runtime.

2. The maven-compiler-plugin configuration:

We need to configure the compiler plugin to actually run the annotation processor. Add this inside your <build><plugins>...</plugins></build> section.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version> <!-- Use a recent version -->
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.hibernate.orm</groupId>
                <artifactId>hibernate-jpamodelgen</artifactId>
                <version>${hibernate.version}</version>
            </path>
            <!-- Add Lombok if you use it -->
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Your final pom.xml sections should look something like this:

<properties>
    ...
    <!-- Spring Boot parent already defines hibernate.version -->
</properties>

<dependencies>
    ...
    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-jpamodelgen</artifactId>
        <scope>provided</scope>
    </dependency>
    ...
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.hibernate.orm</groupId>
                        <artifactId>hibernate-jpamodelgen</artifactId>
                        <version>${hibernate.version}</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Step 4: Generate the Classes!

Now, run the Maven build command from your project’s root directory:

mvn clean install

If everything is configured correctly, Maven will compile your code, the hibernate-jpamodelgen processor will run, and it will generate the metamodel class.

You can find the generated file in: target/generated-sources/annotations/com/example/metamodel/Book_.java.

It will look like this:

// Generated at ...
package com.example.metamodel;

import jakarta.persistence.metamodel.SingularAttribute;
import jakarta.persistence.metamodel.StaticMetamodel;
import java.time.LocalDate;
import javax.annotation.processing.Generated;

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

	public static volatile SingularAttribute<Book, LocalDate> publicationDate;
	public static volatile SingularAttribute<Book, String> author;
	public static volatile SingularAttribute<Book, Long> id;
	public static volatile SingularAttribute<Book, String> title;
	public static volatile SingularAttribute<Book, String> isbn;
        public static volatile SetAttribute<Book, String> tags;

	public static final String PUBLICATION_DATE = "publicationDate";
	public static final String AUTHOR = "author";
	public static final String ID = "id";
	public static final String TITLE = "title";
	public static final String ISBN = "isbn";
        public static final String TAGS = "tags";

}

Key attribute types you’ll see

  • SingularAttribute<X, T>: a single-valued attribute (e.g., String title)
  • SetAttribute<X, E> / ListAttribute<X, E> / MapAttribute<X, K, V>: collection-valued attributes

IDE Tip: Your IDE might not recognize this generated sources folder automatically. In IntelliJ IDEA, right-click the target/generated-sources/annotations directory and select Mark Directory as -> Generated Sources Root. This will allow the IDE to find Book_ and provide autocompletion.

Using the Metamodel in a Custom Repository

Let’s create a custom repository method that uses the Criteria API and our shiny new metamodel.

src/main/java/com/example/metamodel/BookRepositoryCustom.java

package com.example.metamodel;

import java.util.List;

public interface BookRepositoryCustom {
    List<Book> findBooksByAuthor(String author);
}

src/main/java/com/example/metamodel/BookRepositoryCustomImpl.java

package com.example.metamodel;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import java.util.List;

public class BookRepositoryCustomImpl implements BookRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Book> findBooksByAuthor(String author) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Book> query = cb.createQuery(Book.class);
        Root<Book> book = query.from(Book.class);

        // Here it is! The type-safe way.
        Predicate authorPredicate = cb.equal(book.get(Book_.author), author);
        query.where(authorPredicate);
        
        return entityManager.createQuery(query).getResultList();
    }
}

Finally, extend your main repository interface:

src/main/java/com/example/metamodel/BookRepository.java

package com.example.metamodel;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Long>, BookRepositoryCustom {
}

Now you can inject BookRepository and call findBooksByAuthor("some-name"), and it will execute a query that was built in a completely type-safe manner.

Conclusion

Setting up the JPA Metamodel in a Spring Boot 3 project requires a small amount of Maven configuration, but the payoff is enormous. You trade a few minutes of setup for a massive gain in code quality, maintainability, and developer confidence. You can eliminate an entire class of runtime errors and make your data access layer more resilient to change.

Picture of hanguyenvan

hanguyenvan

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top