1. Introduction

Jakarta Persistence (formerly JPA) is the standard API for object-relational mapping in Java. It enables developers to manage relational data in Java applications and simplifies database interactions by mapping Java objects to database tables using annotations and entity classes.

In this tutorial, we’ll explore some of the key new features introduced in Jakarta Persistence 3.2, highlighting improvements in configuration, performance, and usability.

2. What Is Jakarta Persistence 3.2?

Jakarta Persistence 3.2 is the latest version of the Jakarta Persistence API, which provides a standardized approach for object-relational mapping (ORM) in Java applications.

This version introduces improvements in query capabilities, performance, usability, and enhanced support for modern database features.

To add support for Jakarta Persistence 3.2, we must add the following Maven dependency to our pom.xml:

<dependency>
    <groupId>jakarta.persistence</groupId>
    <artifactId>jakarta.persistence-api</artifactId>
    <version>3.2.0</version>
</dependency>

Additionally, we need the latest Hibernate 7 version, which supports this API:

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>7.0.0.Beta1</version>
</dependency>

3. Key New Features

Jakarta Persistence 3.2 introduces a few new features to improve database connection handling, schema configuration, and transaction management.

3.1. Persistence Configuration

The latest Jakarta Persistence 3.2 version adds programmatic API to obtain an instance of the EntityManagerFactory interface using the PersistenceConfiguration class instead of the traditional persistence.xml file – providing flexibility, especially in environments where runtime configurations may vary.

To demonstrate the new features and enhancements, let’s create the Employee entity class with a few fields like id, fullName, and department:

@Entity
public class Employee {
    @Id
    private Long id;

    private String fullName;

    private String department;

    // getters and setters ...
}

Here, the @Entity annotation indicates that the Employee class is a persistent entity and the @Id annotation marks the id field as the primary key.

Now, let’s programmatically configure an instance of the EntityManagerFactory class using the newly introduced PersistenceConfiguration class:

EntityManagerFactory emf = new PersistenceConfiguration("EmployeeData")
  .jtaDataSource("java:comp/env/jdbc/EmployeeData")
  .managedClass(Employee.class)
  .property(PersistenceConfiguration.LOCK_TIMEOUT, 5000)
  .createEntityManagerFactory();

assertNotNull(emf);

We create the instance of the EntityManagerFactory by setting up the data source, registering the entity class, and configuring properties like lock timeouts.

3.2. Schema Manager API

The new version of Jakarta Persistence also introduces the Schema Manager API, allowing developers to manage schema programmatically. This simplifies database migrations and schema validation in both development and production environments.

For instance, we can now enable schema creation using the API:

emf.getSchemaManager().create(true);

In total, there are four functions available for schema management:

  • create(): creates the tables associated with entities in the persistence unit
  • drop(): drops tables associated with entities in the persistence unit
  • validate(): validates the schema against the entity mappings
  • truncate(): clears data from tables related to entities

3.3. Run/Call in Transaction

There are now new methods like runInTransaction() and callInTransaction() to improve the handling of database transactions by providing an application-managed EntityManager with an active transaction.

With these methods, we can perform operations within a transaction scope and access the underlying database connection when necessary.

We can use these methods to run a query within a transaction and operate directly on the database connection:

emf.runInTransaction(em -> em.runWithConnection(connection -> {
    try (var stmt = ((Connection) connection).createStatement()) {
        stmt.execute(
          "INSERT INTO employee (id, fullName, department) VALUES (8, 'Jane Smith', 'HR')"
        );
    } catch (Exception e) {
        Assertions.fail("JDBC operation failed");
    }
}));

var employee = emf.callInTransaction(em -> em.find(Employee.class, 8L));

assertNotNull(employee);
assertEquals("Jane Smith", employee.getFullName());

First, we’ve executed SQL to insert a new employee into the database within a transaction using the runInTransaction(). Then, the callInTransaction() method retrieves and verifies the inserted employee’s details.

3.4. TypedQueryReference Interface

Named queries are usually referenced by strings in Jakarta Persistence making them prone to errors such as typos in the query name.

The newly introduced TypedQueryReference interface aims to solve this by linking named queries to the static metamodel, thus making them type-safe and discoverable at compile-time.

Let’s update our Employee entity with a named query to search using the department field:

@Entity
@NamedQuery(
  name = "Employee.byDepartment",
  query = "FROM Employee WHERE department = :department",
  resultClass = Employee.class
)
public class Employee {
// ...
}

Once compiled, the corresponding static metamodel would be generated as follows:

@StaticMetamodel(Employee.class)
@Generated("org.hibernate.processor.HibernateProcessor")
public abstract class Employee_ {
    public static final String QUERY_EMPLOYEE_BY_DEPARTMENT = "Employee.byDepartment";
    public static final String FULL_NAME = "fullName";
    public static final String ID = "id";
    public static final String DEPARTMENT = "department";

    // ...
}

Now, we can use the QUERY_EMPLOYEE_BY_DEPARTMENT constant to refer to the named query byDepartment defined on the Employee entity:

Map<String, TypedQueryReference> namedQueries = emf.getNamedQueries(Employee.class);

List employees = em.createQuery(namedQueries.get(QUERY_EMPLOYEE_BY_DEPARTMENT))
  .setParameter("department", "Science")
  .getResultList();

assertEquals(1, employees.size());

In the code snippet, we can observe that the getNamedQueries() method of EntityManagerFactory returns a map of the named query and its TypedQueryReference. Then, we used the EntityManager‘s createQuery() method to get the employees from the Science department and assert that the list contains exactly one result, confirming the query’s expected output.

Therefore, the TypedQueryReference interface ensures that the named query exists and is correctly referenced, providing compile-time validation.

3.5. Type-safety in EntityGraph

Jakarta Persistence’s entity graphs allow the eager loading of properties when executing a query.

Now, with the new version of Jakarta Persistence, they are type-safe too – ensuring properties referenced in the graph are valid and exist at compile time, reducing the risk of errors.

For example, let’s use the static metamodel Employee_ to ensure type safety at compile time:

var employeeGraph = emf.callInTransaction(em -> em.createEntityGraph(Employee.class));
employeeGraph.addAttributeNode(Employee_.department);

var employee = emf.callInTransaction(em -> em.find(employeeGraph, 7L));

assertNotNull(employee);
assertEquals("Engineering", employee.getDepartment());

Here, the property department is accessed from the static meta-model class which validates that it exists in the Employee class, otherwise, this creates a compilation error if we get the property wrong.

4. Usability Enhancements

Jakarta Persistence 3.2 introduces several performance and usability enhancements to simplify database queries and improve overall application performance.

4.1. Streamlined JPQL

This streamlined query syntax is now supported in JPQL and is commonly used in Jakarta Data Query Language, a subset of JPQL.

For example, when an entity doesn’t specify an alias, it automatically defaults to the associated table:

Employee employee = emf.callInTransaction(em -> 
  em.createQuery("from Employee where fullName = 'Tony Blair'", Employee.class).getSingleResult()
);

assertNotNull(employee);

Here, we didn’t specify an alias for the Employee entity. Instead, the alias defaults to this, allowing us to perform operations directly on the entity without needing to qualify field names.

4.2. cast() Function

The new cast() method in the Jakarta Persistence allows us to cast query results:

emf.runInTransaction(em -> em.persist(new Employee(11L, "123456", "Art")));

TypedQuery<Integer> query = em.createQuery(
  "select cast(e.fullName as integer) from Employee e where e.id = 11", Integer.class
);
Integer result = query.getSingleResult();

assertEquals(123456, result);

In this example, we first insert a new Employee record with 123456 as the value for the fullName. Then, using a JPQL query, we cast the String property fullName to an Integer.

4.3. left() and right() Function

Next, JPQL also allows string manipulation methods like left() to extract substrings using the index value:

TypedQuery<String> query = em.createQuery(
  "select left(e.fullName, 3) from Employee e where e.id = 2", String.class
);
String result = query.getSingleResult();

assertEquals("Tom", result);

Here, we’ve extracted the substring Tom from the left of the fullName using the JPQL functions left().

Similarly, it also provides the right() method for substring extraction:

query = em.createQuery("select right(e.fullName, 6) from Employee e where e.id = 2", String.class);
result = query.getSingleResult();

assertEquals("Riddle", result);

So, as demonstrated, we’ve extracted the substring Riddle from the right of the fullName.

4.4. replace() Function

Similarly, the replace() function is also available in JPQL now, allowing us to replace part of the String:

TypedQuery<String> query = em.createQuery(
  "select replace(e.fullName, 'Jade', 'Jane') from Employee e where e.id = 4", String.class
);
String result = query.getSingleResult();

assertEquals("Jane Gringer", result);

Here, the replace() function has substituted the occurrences of Jade to the new String value Jane in the fullName property.

4.5. id() Function

Additionally, the new id() method lets us extract the identifier of the database record:

TypedQuery<Long> query = em.createQuery(
  "select id(e) from Employee e where e.fullName = 'John Smith'", Long.class
);
Long result = query.getSingleResult();

assertEquals(1L, result);

The id() function fetches the primary key of the Employee record matching the fullName to John Smith.

4.6. Improved Sorting

Finally, sorting improvements for Jakarta Persistence 3.2 add null-first and case-insensitive ordering using scalar expressions like lower() and upper():

emf.runInTransaction(em -> {
    em.persist(new Employee(21L, "alice", "HR"));
    em.persist(new Employee(22L, "Bob", "Engineering"));
    em.persist(new Employee(23L, null, "Finance"));
    em.persist(new Employee(24L, "charlie", "HR"));
});

TypedQuery<Employee> query = em.createQuery(
  "SELECT e FROM Employee e ORDER BY lower(e.fullName) ASC NULLS FIRST, e.id DESC", Employee.class
);

List<Employee> sortedEmployees = query.getResultList();

assertNull(sortedEmployees.get(0).getFullName());
assertEquals("alice", sortedEmployees.get(1).getFullName());
assertEquals("Bob", sortedEmployees.get(2).getFullName());
assertEquals("charlie", sortedEmployees.get(3).getFullName());

In this example, we’ve sorted the Employee records by fullName in case-insensitive ascending order (using the lower() function), with null values first, and by the id descending.

5. Conclusion

In this article, we’ve discussed the latest Jakarta Persistence 3.2 with a host of new features and improvements to streamline ORM operations and efficient data handling.

We covered features including simplified persistence configuration, programmatic schema management, and transaction management. Then, we explored usability improvements to JPQL providing additional functions to write better queries.

The complete code for this article is available over on GitHub.