Skip to content

Jakarta Persistence

The Jakarta Persistence API (JPA) is the standard for persistence and object-relational mapping in Java environments. It provides a straightforward API for executing queries using the Jakarta Persistence Query Language (JPQL), as well as an alternative Criteria API for constructing type-safe queries in Java code.

As a minor upgrade, Jakarta Persistence 3.2 adds numerous enhancements and new features. For a comprehensive list, see the Jakarta Persistence 3.2 specification page.

JPQL Improvements

Jakarta Persistence 3.2 refines the Jakarta Persistence Query Language (JPQL), introducing new syntax and porting several SQL functions.

Queries Without a Select Clause

This feature, long available in Hibernate, is now standardized in Jakarta Persistence 3.2:

em.createQuery("from Book where name like '%Hibernate'", Book.class)
    .getResultStream()
    .forEach(book -> LOG.debug("query result without select:{}", book));

Here, the select keyword is omitted. The query in the above code is equivalent to the following classic form:

select b from Book b ...

New Functions: id(this), count(this), version(this)

Jakarta Persistence 3.2 introduces the functions id(this), count(this), and version(this), allowing you to query an entity's ID, count, and version, respectively:

// count book entity
var count = em.createQuery("select count(this) from Book")
    .getSingleResult();
LOG.debug("count(this) result: {}", count);

// id and version
em.createQuery("select id(this), version(this) from Book", Object[].class)
    .getResultList()
    .forEach(book -> LOG.debug("id and version result: {}", book));

String Concatenation

In 3.2, JPQL now supports SQL-style string concatenation using ||:

// Query books where the author's name matches the person's first and last name
em.createQuery("""
        select b from Book b cross join Person c
        where b.author.name = c.firstName || ' ' || c.lastName
          and c.firstName = :firstName
          and c.lastName = :lastName
        """, Book.class)
    .setParameter("firstName", "Gavin")
    .setParameter("lastName", "King")
    .getResultStream()
    .forEach(book -> LOG.debug("author name equals person name: {}", book));

Null Handling in the ORDER BY Clause

The nulls first and nulls last features, previously available only in SQL, are now supported in JPQL:

em.createQuery("from Book order by name nulls first", Book.class)
    .getResultStream()
    .forEach(book -> LOG.debug("sorted with nulls first: {}", book));

This query ensures that results with null in the name field appear first.

Additional SQL Functions: left, right, cast, replace

Standard SQL functions such as left, right, cast, and replace are now available in JPQL, reducing the need to fall back to native queries:

em.createQuery("""
        select left(name, 5),
               right(name, 2),
               cast(price as Integer),
               replace(name, ' ', '_'),
               name
        from Book
        """, Object[].class)
    .getResultStream()
    .forEach(book -> LOG.debug("new functions result: {}", java.util.Arrays.toString(book)));

Set Operations: union, intersect, and except

Several set operators in SQL, such as union, intersect, and except, have also been introduced in JPQL. These operators allow you to combine, compare, or subtract the results of two or more SELECT queries, treating the results as mathematical sets. Let’s examine some examples to illustrate their usage.

The following query combines person full names and book author names, returning a distinct list of all names.

// query union book name and person name
em.createQuery("""
            select c.firstName ||' '|| c.lastName from Person c
            union
            select b.author.name  from Book b
            """, String.class)
    .getResultStream()
    .forEach(name -> LOG.debug("query union book name and person name: {}", name));

This query returns names that exist both as person full names and book author names.

// intersect book name and person name
em.createQuery("""
            select c.firstName ||' '|| c.lastName from Person c
            intersect
            select b.author.name  from Book b
            """, String.class)
    .getResultStream()
    .forEach(name -> LOG.debug("intersect book name and person name: {}", name));

This query returns the person's full names that are not the book author's names.

// except book name and person name
em.createQuery("""
            select c.firstName ||' '|| c.lastName from Person c
            except
            select b.author.name  from Book b
            """, String.class)
    .getResultStream()
    .forEach(name -> LOG.debug("except book name and person name: {}", name));

While we've covered the exciting changes to JPQL syntax, it's worth noting that all these enhancements are also fully supported by the Criteria API. This means you can harness the power of these new features using type-safe Java code, adding another layer of robustness to your queries. The Criteria API also introduces subtle improvements, such as the introduction of the new CriteriaSelect interface, further streamlining query construction.

New CriteriaSelect Interface

The new CriteriaSelect interface is a top-level interface designed to represent a united general-purpose query, including union and intersect operations.

Let's convert the former JPQL union query example into a type-safe Criteria API equivalent:

CriteriaBuilder cb = em.getCriteriaBuilder();

// First part of the union: select c.firstName || ' ' || c.lastName from Person c
CriteriaQuery<String> personQuery = cb.createQuery(String.class);
Root<Person> personRoot = personQuery.from(Person.class);
personQuery.select(cb.concat(List.of(personRoot.get(Person_.FIRST_NAME), cb.literal(" "), personRoot.get(Person_.LAST_NAME))));

// Second part of the union: select b.author.name from Book b
CriteriaQuery<String> bookQuery = cb.createQuery(String.class);
Root<Book> bookRoot = bookQuery.from(Book.class);
bookQuery.select(bookRoot.get("author").get("name"));

// Combine the two queries with UNION
// Jakarta Persistence 3.2 adds union() to CriteriaBuilder
CriteriaSelect<String> unionQuery = cb.union(personQuery, bookQuery);

em.createQuery(unionQuery)
        .getResultStream()
        .forEach(name -> LOG.info("query union book name and person name: " + name));

As you can see, when you combine two queries using CriteriaBuilder.union(...), the method now returns a CriteriaSelect object, not a CriteriaQuery. This CriteriaSelect acts as the parent interface for the existing CriteriaQuery, providing a unified way to manage these set operations.

Entity Mapping Improvements

Jakarta Persistence 3.2 introduces several enhancements to the Entity classes mappings.

Package-Level Generator Definitions

Before 3.2, when using SequenceGenerator or TableGenerator, you had to declare them with @GeneratedValue in the entity classes like this.

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="blog_seq")
    @SequenceGenerator(name = "blog_seq", initialValue = 1, allocationSize = 10)
    private Long id;
    // ...
}

It could be tedious to set up in every class.

Starting with version 3.2, Jakarta Persistence allows you to define identity generators at the package level. When a generator is declared in a package-info.java file, it will be automatically applied to all entity classes within that package according to generator types.

For example, you can declare generators as follows in your package-info.java:

@SequenceGenerator(name = "blog_seq", initialValue = 1, allocationSize = 10)
@TableGenerator(
    name = "tbl_id_gen",
    table = "id_gen",
    pkColumnName = "gen_key",
    pkColumnValue = "id",
    valueColumnName = "gen_val",
    allocationSize = 10
)
package com.example.blog;

import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.TableGenerator;

Once defined, your entity classes can reference these generators:

@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    // ...
}

@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "tbl_id_gen")
    private Long id;
    // ...
}

The persistence provider will automatically discover the generators defined in package-info.java and apply them to the corresponding entities.

Ongoing Java 8 DateTime API Enhancements

Before 3.2, Java 8 DateTime APIs were already supported in Jakarta Persistence as alternatives to the legacy Date types from the java.util and java.sql packages.

Starting with version 3.2, the support for legacy Date, Calendar, Time, and java.sql.Date types is now deprecated. It is recommended to use the modern Java 8 DateTime API instead when starting a new project.

Additionally, Instant and Year are now supported as basic types:

class Book {
    Instant createdAt;
    Year publicationYear;
}

Version 3.2 also introduces support for using LocalDateTime and Instant as the entity version type:

class Book {
    @Version
    Instant version;
}

With the deprecation of the legacy date types, annotations such as @Temporal are also deprecated in 3.2.

New Attributes in @Column Annotation

Jakarta Persistence 3.2 introduces two new attributes to the @Column annotation: comment and check, offering richer schema generation capabilities.

@Entity
class Post {

    @Column(
        name = "title",
        nullable = false,
        length = 100,
        unique = true,
        comment = "Post title",
        check = @CheckConstraint(
            name = "title_min_length",
            constraint = "length(title) > 10"
        )
    )
    private String title;

    // ...
}

The new check attribute allows you to define check constraints at the column level, which will be reflected in the generated database schema:

title VARCHAR(100) NOT NULL UNIQUE /* Post title */ CHECK (length(title) > 10)

Another improvement in 3.2 is the secondPrecision attribute, which can be set on temporal columns to control the precision of persisted timestamp values. This is particularly useful for ensuring consistency across different persistence providers and various databases.

@Column(name = "created_at", secondPrecision = 3)
private Instant createdAt;

This addresses previous issues where different JPA providers handled timestamp precision inconsistently. For example, I encountered this while contributing to Eclipse Cargo Tracker.

Customizing Enum Mapping with @EnumeratedValue

Before 3.2, Java enum types could only be mapped using their name or ordinal value with the @Enumerated annotation:

@Entity
class Post {
    @Enumerated(EnumType.STRING)
    private ModerationStatus status;
    // ...
}

public enum ModerationStatus {
    PENDING,
    APPROVED,
    REJECTED
}

In 3.2, it introduces a new annotation - @EnumeratedValue, which allows you to specify a custom field of the Enum type to be persisted:

@Entity
class Post {
    private ModerationStatus status;
    // ...
}

public enum ModerationStatus {
    PENDING(0),
    APPROVED(1),
    REJECTED(-1);

    @EnumeratedValue
    private final int value;

    ModerationStatus(int value) {
        this.value = value;
    }
}

Now, it will store the field value marked with @EnumeratedValue instead of the enum name or ordinal.

Record Types as Embeddables

Java record type support is a significant addition in Jakarta EE 11. With Jakarta Persistence 3.2, Java records are now fully supported and can be used as @Embeddable types. For more details, please move on to the dedicated Java Record Support in Jakarta EE 11 document.

API Enhancements

Several minor developer-oriented improvements have been added to the EntityManager and Query, which will enhance development productivity.

Programmatic Configuration

Before version 3.2, in a Java SE environment, creating an EntityManagerFactory required a persistence.xml file placed in the src/main/resources/META-INF directory of your project.

Here is an example of a persistence.xml:

<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_2.xsd"
             version="3.2">

    <persistence-unit name="bookstorePU" transaction-type="RESOURCE_LOCAL">

        <description>Hibernate test case template Persistence Unit</description>
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>

        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="hibernate.archive.autodetection" value="class, hbm"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.connection.driver_class" value="org.h2.Driver"/>
            <property name="hibernate.connection.url" value="jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1"/>
            <property name="hibernate.connection.username" value="sa"/>
        </properties>
    </persistence-unit>
</persistence>

Then you can create an EntityManagerFactory instance like this:

var emf = Persistence.createEntityManagerFactory("bookstorePU");

With Jakarta Persistence 3.2, the new PersistenceUnitConfiguration allows you to set up these properties programmatically using the builder-style pattern:

PersistenceConfiguration configuration = new PersistenceConfiguration("bookstore")
    .transactionType(PersistenceUnitTransactionType.RESOURCE_LOCAL)
    .provider(HibernatePersistenceProvider.class.getName())
    // .nonJtaDataSource("java:global/jdbc/BookstoreData")
    .managedClass(Book.class)
    .managedClass(Isbn.class)
    .managedClass(Author.class)
    .property(PersistenceConfiguration.LOCK_TIMEOUT, 5000)
    .property("hibernate.type.prefer_java_type_jdbc_types", true)
    .property("hibernate.hbm2ddl.auto", "create-drop")
    .property(PersistenceConfiguration.JDBC_URL, "jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1")
    .property(PersistenceConfiguration.JDBC_DRIVER, "org.h2.Driver")
    .property(PersistenceConfiguration.JDBC_USER, "sa");

Then you can create an EntityManagerFactory instance using the following method:

var emf = configuration.createEntityManagerFactory();

Or using the new Persistence.createEntityManagerFactory method variant, which accepts a PersistenceConfiguration parameter:

var emf = Persistence.createEntityManagerFactory(configuration);

Schema Management

Before version 3.2, you could configure using script files, entities, or both to manage database schema generation at runtime.

In the following example, we configure the schema-generation action and export the database schema using properties in persistence.xml:

<persistence ...>
    <persistence-unit>
        <properties>
            <property name="jakarta.persistence.schema-generation.scripts.action" value="drop-and-create" />
            <property name="jakarta.persistence.schema-generation.scripts.create-target" value="/tmp/create.ddl" />
            <property name="jakarta.persistence.schema-generation.scripts.drop-target" value="/tmp/drop.ddl" />
        </properties>
    </persistence-unit>
</persistence>

You could then use the Persistence.generate(String persistenceUnit, Map<String, Object> properties) method to generate schema scripts at the specified target paths.

Jakarta Persistence 3.2 introduces the new SchemaManager, which allows you to validate, create, drop, and truncate the database schema according to the existing persistence configuration:

emf.getSchemaManager().validate();
emf.getSchemaManager().truncate();
emf.getSchemaManager().drop(true);    // if true, applies changes to the database
emf.getSchemaManager().create(true);  // if true, applies changes to the database

The truncate method could help reset the database when writing test code.

[!Note] The SchemaManager does not support exporting the schema to DDL script files.

And unfortunately, in 3.2, the Persistence.generate does not involve a variant and accepts a PersistenceConfiguration parameter (i.e., Persistence.generate(PersistenceConfiguration) does not exist).

Functional Transactions

Before 3.2, you could control the transaction boundaries manually as follows:

EntityTransaction tx = em.getTransaction();
tx.begin();
try {
    // ...
    em.persist(entity);
    tx.commit();
} catch (Exception e) {
    tx.rollback();
}

Jakarta Persistence 3.2 introduces two new methods, runInTransaction and callInTransaction, on EntityManagerFactory, which allow you to execute logic within a transactional context.

The following is an example of using runInTransaction to persist a Book entity, and there is no need to return a result. It is suitable for mutating operations such as insert, update, or delete.

emf.runInTransaction(em -> {
    Book entity = new Book(
        new Isbn("9781932394887"),
        "Java Persistence with Hibernate",
        new Author("Gavin King"),
        new BigDecimal("50.1234")
    );
    em.persist(entity);
    LOG.debug("persisted book: {}", entity);
});

Alternatively, the callInTransaction method is designed for cases that require returning a result after the logic is executed. It is ideal for selection queries.

emf.callInTransaction(em -> em.createQuery("from Book", Book.class)
                             .getResultList())
    .forEach(book -> LOG.debug("saved book: {}", book));

With these methods, you no longer need to control transactions explicitly, such as begin, commit, and rollback. Every execution block automatically participates in an active transaction context.

Additionally, the EntityManager adds two similar methods: runWithConnection and callWithConnection, which bind database operations to an immutable Connection abstraction. Typically, for the databases using JDBC, these methods let you work with a JDBC Connection object.

Here’s how to use runWithConnection:

em.runWithConnection(conn -> {
    var rs = conn.prepareStatement("select * from posts").executeQuery();
    while (rs.next()) {
        LOG.debug("query result:");
        LOG.debug("id: {}", rs.getLong("id"));
        LOG.debug("title: {}", rs.getString("title"));
        LOG.debug("content: {}", rs.getString("content"));
    }
});

This method is transaction-aware and joins any existing transaction. You don’t need to manage transactions and care about the lifecycle of the incoming Connection object. Do not try to close the Connection yourself inside the block.

Type-Safe Options

In Jakarta Persistence 3.2, the 'EntityManager' overloads methods such as find, refresh, and lock to accept type-safe FindOption, RefreshOption, and LockOption respectively, replacing the previous use of a generic Map<String, Object> properties.

Before 3.2, you could tune the find method with a general Map parameter.

// Using a Map for query hints and options
var book = em.find(Book.class, new Isbn("9781932394887"),
        Map.of("jakarta.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS,
               "jakarta.persistence.query.timeout", 500,
               "org.hibernate.readOnly", true)
);
LOG.debug("Found book using Map-based options: {}", book);

In 3.2, you can use the type-safe options instead.

// Using type-safe options
var result = em.find(Book.class, new Isbn("9781932394887"),
        CacheRetrieveMode.BYPASS,
        Timeout.seconds(500),
        LockModeType.READ);
LOG.debug("Found book using type-safe options: {}", result);

Besides these, the generated static metamodel includes strongly typed constants for named queries, entity graphs, result set mappings, and managed types, thereby minimizing typos and making refactoring safer.

Suppose you have the following entity:

@NamedQuery(name = "byTitle", query = "SELECT p FROM Post p WHERE p.title = :title")
@NamedEntityGraph(
        name = "withComments",
        attributeNodes = {
                @NamedAttributeNode("title"),
                @NamedAttributeNode("content"),
                @NamedAttributeNode(value = "comments", subgraph = "commentsGraph"),
                @NamedAttributeNode("createdAt")
        },
        subgraphs = @NamedSubgraph(
                name = "commentsGraph",
                attributeNodes = @NamedAttributeNode("content")
        )
)
public class Post { ... }

After compilation, the generated Post_ metamodel class will contain constants for the named query byTitle and named entity graph withComments:

public abstract class Post_ {
    // ...
    public static final String QUERY_BY_TITLE = "byTitle";
    public static final String GRAPH_WITH_COMMENTS = "withComments";
}

Then you can use the constants for queries and entity graphs to replace the former literal text.

// Referencing the named query using the metamodel constant
var result = em.createNamedQuery(Post_.QUERY_BY_TITLE, Post.class)
        .setParameter("title", "What's new in Persistence 3.2?")
        .getSingleResult();
LOG.debug("Query byTitle result: {}", result);

// Referencing the named entity graph using the metamodel constant
Post result2 = em.find(Post.class, entity.getId(),
        em.getEntityGraph(Post_.GRAPH_WITH_COMMENTS));
LOG.debug("Query withComments result: {}", result2.getComments());

You can also refer to attribute names via constants in the static metamodel in relationship mappings:

@Entity
public class Post {
    // ...
    @OneToMany(mappedBy = Comment_.POST,
            fetch = FetchType.LAZY,
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private Set<Comment> comments = new HashSet<>();
}

This approach ensures type safety and helps avoid errors caused by hard-coded string references.

New Method getSingleResultOrNull in Query

Before version 3.2, the getSingleResult method would return the single unique result if it existed. Otherwise, it would throw a NoResultException.

To return null instead of throwing exceptions, you had to write something like:

try {
   return query.getSingleResult();
} catch (NoResultException e) {
   return null;
}

Jakarta Persistence 3.2 solves this by introducing the new getSingleResultOrNull method to Query and its derived interfaces, including TypedQuery<T>, among others. This method returns null directly when no result is found.

Here is an example using getSingleResultOrNull:

var nullableResult = em.createQuery("from Book where id = :isbn", Book.class)
        .setParameter("isbn", new Isbn("9781932394887"))
        .getSingleResultOrNull();
LOG.debug("book getSingleResultOrNull result: {}", nullableResult);

Now you never have to worry about catching a NoResultException.

[!Note] When representing the presence or absence of a single result, I would prefer to use Optional<T> to align with modern Java best practices. See the issue: jakartaee/persistence#479.

New Method getReference(T) in EntityManager

As an alternative to the existing getReference(Class, id), the new method provides a way to obtain a reference to an entity using a given object with the same primary key. The supplied object may be in a managed or detached state, but it must not be new or removed.

This method is beneficial when you need to set an association using detached entity instances, for example:

var post = ...
// The session is closed here, so the `post` instance is now detached

// In a new session
comment.setPost(em.getReference(post));
// Persist the comment and close the session

This approach avoids the need to fetch the post entity from the database again.

Here, we highlight the API improvements in Jakarta Persistence 3.2 that offer tangible benefits to application developers. While there are many other minor enhancements not covered here, you can find the complete list of changes in the Jakarta Persistence 3.2 specification.

Jakarta EE Integration

In Jakarta EE environments, before 3.2, you should use @PersistenceContext to inject an EntityManager bean that matches the default persistence unit definition in the persistence.xml file:

@PersistenceContext
private EntityManager em;

When it comes to 3.2, you no longer need to use @PersistenceUnit or @PersistenceContext to inject EntityManagerFactory or EntityManager in the Jakarta EE components. Instead, you can use standard CDI @Inject to inject them like injecting regular CDI beans.

@Inject
private EntityManager em;

Jakarta Persistence 3.2 also allows you to specify scope and qualifier elements in your persistence.xml file, making it easier to control the lifecycle and selection of persistence units in CDI.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.2" xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_2.xsd">
    <persistence-unit name="defaultPU" transaction-type="JTA">
        <qualifier>com.example.employee.MyCustom</qualifier>
        <scope>jakarta.enterprise.context.ApplicationScoped</scope>
        <jta-data-source>java:comp/DefaultDataSource</jta-data-source>
        ...
    </persistence-unit>
</persistence>

Here, MyCustom is a custom CDI @Qualifier annotation:

@Documented
@Retention(RUNTIME)
@Qualifier
public @interface MyCustom {
}

Then you can inject the qualified EntityManager or EntityManagerFactory as follows:

@Inject @MyCustom
private EntityManager em;

Example Projects

All sample code referenced in this guide is available on GitHub. You can explore and try it out for yourself.

Hibernate Example Project

You can find the Hibernate example here: https://github.com/hantsy/jakartaee11-sandbox/tree/master/hibernate, which demonstrates running Jakarta Persistence code in the Java SE environment.

Check out the source code, and import the project into your favorite IDE.

Open the pom.xml file, and you will see the project includes Hibernate ORM and the Jakarta Persistence API dependencies:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>${hibernate.version}</version>
</dependency>
<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-scan-jandex</artifactId>
    <version>${hibernate.version}</version>
</dependency>
<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.2.0</version>
</dependency>

To generate static metamodel classes for your entities at compile time, you should add hibernate-processor to the annotationProcessorPaths section of the maven-compiler-plugin configuration:

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven-compiler-plugin.version}</version>
        <configuration>
            <annotationProcessorPaths>
                <annotationProcessorPath>
                    <groupId>org.hibernate.orm</groupId>
                    <artifactId>hibernate-processor</artifactId>
                    <version>${hibernate.version}</version>
                </annotationProcessorPath>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
</plugins>

You can just run the test code in the IDE directly to see Jakarta Persistence 3.2 features in action.

Jakarta EE Example Project

The Jakarta EE example is available at: https://github.com/hantsy/jakartaee11-sandbox/tree/master/persistence. This sample project demonstrates the integration of Jakarta Persistence 3.2 and CDI in the Jakarta EE environment. It requires Jakarta EE application servers, such as GlassFish 8.x or WildFly Preview 37+, to run the sample project.

In this project, you do not need to add an extra persistence provider dependency. Jakarta EE application servers contain a built-in Persistence provider that is shared among all applications running on the server.

Here we configured EclipseLink to generate static metamodel classes(of course, the previous hibernate-processor also works):

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven-compiler-plugin.version}</version>
        <configuration>
            <parameters>true</parameters>
            <annotationProcessorPaths>
                <annotationProcessorPath>
                    <groupId>org.eclipse.persistence</groupId>
                    <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
                    <version>${eclipselink.version}</version>
                </annotationProcessorPath>
            </annotationProcessorPaths>
        </configuration>
    </plugin>
</plugins>

To run the tests on a managed Glassfish, execute the following command in a terminal window:

mvn clean verify -Parq-managed-glassfish

It utilizes the GlassFish Managed Adapter for Arquillian and runs tests on the real GlassFish servers.

[!NOTE] For more information about Arquillian, visit https://www.arquillian.org.

Summary

In summary, Jakarta Persistence 3.2 introduces a range of enhancements, such as improved JPQL syntax, backported SQL functions, modernized support for Java Date and Time types, streamlined configuration options, and deeper integration with Jakarta EE and CDI. For comprehensive details, see the Jakarta Persistence 3.2 specification.