Exploring Jakarta Persistence 3.2
Jakarta Persistence (formerly JPA) is a key part of the Jakarta EE ecosystem, enabling developers to seamlessly map Java objects to relational database tables. Jakarta Persistence 3.2 introduces several notable enhancements that improve our productivity and enhance the framework’s capabilities. This article will explore some of the key features introduced in Jakarta Persistence 3.2, highlighting their benefits through examples and configurations.
1. Programmatic Configuration (Optional persistence.xml)
Before Jakarta Persistence 3.2, JPA relied on the persistence.xml
file for configuring entities, data sources, and other settings. With 3.2, programmatic configuration is now a key feature, allowing us to define the persistence unit entirely in Java code. This eliminates the need for a persistence.xml
file in many cases, providing greater flexibility and enabling more dynamic configurations.
Traditionally, the persistence.xml
file located in the META-INF
folder was required to define persistence units like this:
<!-- persistence.xml --> <persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0"> <persistence-unit name="examplePU"> <class>com.example.Entity</class> <properties> <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:test" /> <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="jakarta.persistence.jdbc.user" value="sa" /> <property name="jakarta.persistence.jdbc.password" value="" /> </properties> </persistence-unit> </persistence>
With 3.2, persistence units can now be configured entirely programmatically, allowing us to define an EntityManagerFactory
directly in Java code without requiring a persistence.xml
file.
EntityManagerFactory emf = new PersistenceConfiguration("CustomerPU"). jtaDataSource("java:global/jdbc/CustomerData") .managedClass(Customer.class) .property(PersistenceConfiguration.LOCK_TIMEOUT, 5000) .createEntityManagerFactory();
2. Programmatic Schema Export
The SchemaManager API enables us to manage schemas programmatically, complementing the programmatic creation of the EntityManagerFactory
.
emf.getSchemaManager().create(true);
The SchemaManager API provides additional capabilities, including drop()
, which removes tables associated with entities in the persistence unit; validate()
, which checks the schema for consistency with the entity mappings; and truncate()
, which clears all data from tables related to the entities.
3. Using Java Records as Embeddable Entities
Before Jakarta Persistence 3.2, embeddable types were usually defined as separate classes, but now Java Records can be used, offering a more concise and readable approach.
@Embeddable public record Address(String street, String city, String zipCode) { } @Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Embedded private Address address; // Constructors, Getters and Setters }
Here is an explanation of the above code snippet:
@Embeddable
with Java Record: TheAddress
record is marked with the@Embeddable
annotation, indicating that it can be embedded into another entity.- A record in Java is an immutable data class with a concise syntax for defining fields, constructors, and accessors. Here,
Address
has three fields:street
,city
, andzipCode
. - Embedding the
Address
Record: Theaddress
field inCustomer
is annotated with@Embedded
, signifying that theAddress
record is embedded as part of theCustomer
entity.
4. New Transaction Management Methods (runInTransaction
and callInTransaction
)
Jakarta Persistence 3.2 introduces two new methods in the EntityManagerFactory
interface: runInTransaction
and callInTransaction
. These methods simplify transaction management when working with application-managed EntityManager
instances. With these methods, we can handle both write and read operations more efficiently, without manually managing transaction states.
Here’s an example that demonstrates how to use these methods in a Customer
entity:
emf.runInTransaction(em -> em.runWithConnection(connection -> { try (var stmt = ((Connection) connection).createStatement()) { stmt.execute("INSERT INTO customer (name, email) VALUES ('John Doe', 'john.doe@example.com')"); } catch (Exception e) { System.out.println("JDBC operation failed: " + e.getMessage()); } })); Customer customer = emf.callInTransaction(em -> em.find(Customer.class, 1L)); if (customer != null) { System.out.println("Customer found: " + customer.getName() + ", " + customer.getEmail()); } else { System.out.println("Customer not found."); }
The key points are that both runInTransaction
and callInTransaction
automatically manage transaction boundaries, simplifying the interaction with the EntityManager
. These methods abstract away the complexities of transaction handling, eliminating the need for boilerplate code to start, commit, or roll back transactions, while also relieving the developer of the need to write messy exception handling code.
5. Enhancements to JPQL
In the release of Jakarta Persistence 3.2, several enhancements were made to the Java Persistence Query Language (JPQL), incorporating features previously supported as extensions in Hibernate and EclipseLink.
5.1 Simplified Query Syntax for Single Entities
Previously, Hibernate allowed for a more streamlined query syntax without the need to define an alias for the entity. This feature has now been officially incorporated into JPQL. For instance, in the case of a Customer
entity, we can write a query like:
// Simplified query to fetch a single entity Customer customer = emf.callInTransaction(em -> em.createQuery("from Customer where name = 'John Doe'", Customer.class).getSingleResult() );
In the above example, we demonstrate how to use the streamlined query syntax in Jakarta Persistence 3.2 to query the Customer
entity. Notice that the alias for the Customer
entity is not required, and the select
clause is optional. We use the callInTransaction
method to fetch a customer by their name.
The query "from Customer where name = 'John Doe'"
demonstrates the streamlined query syntax introduced in Jakarta Persistence 3.2. There is no need to define an alias for Customer
, and the select
clause is implied since the query returns the entity directly.
5.2 New Standard Functions
In addition to new standard functions added in Jakarta Persistence 3.1, the 3.2 release introduces even more. Functions like cast()
, left()
, right()
, replace()
, id()
, and version()
are now available.
5.2.1 New Standard Function: cast()
The cast()
function, added in version 3.2, allows us to cast expressions to different data types within a query. Here’s how it works with the Customer
class:
// Insert a new customer with a numeric phone number stored as a string emf.runInTransaction(em -> em.persist(new Customer(101L, "John Doe", "1234567890", "john.doe@jcg.com"))); // Use cast() to treat the phoneNumber as an integer TypedQuery<Integer> query = emf.createEntityManager().createQuery( "select cast(c.phoneNumber as integer) from Customer c where c.id = 101", Integer.class ); Integer result = query.getSingleResult(); System.out.println("Phone number as integer: " + result);
A Customer
with an ID of 101 is inserted into the database, with the phoneNumber
field stored as a string (“1234567890”). The cast()
function is then used in a JPQL query to convert the phoneNumber
from a string to an integer, enabling its use in numeric operations or validations.
5.2.2 New Standard Function: id()
The id()
function retrieves the identifier of an entity in a JPQL query. This is how we can use it with the Customer
class:
TypedQuery<Long> query = emf.createEntityManager().createQuery( "select id(c) from Customer c where c.email = 'example@domain.com'", Long.class ); Long result = query.getSingleResult();
This query uses the id()
function to retrieve the primary key of a Customer
entity based on their email
field. For instance, if a customer with an email "example@domain.com"
exists in the database with an ID of 202
, the query will return 202
.
5.2.3 New Standard Function: replace()
The replace()
function, introduced in version 3.2, allows us to replace occurrences of a substring within a string field in JPQL queries. This feature simplifies string manipulation directly within the database layer. Here’s an example demonstrating the use of the replace()
function with the Customer
class:
TypedQuery<String> query = emf.createEntityManager().createQuery( "select replace(c.email, 'oldmail.com', 'newmail.com') from Customer c where c.id = 5", String.class ); String result = query.getSingleResult();
This query replaces the domain part of a Customer
‘s email
field from "oldmail.com"
to "newmail.com"
. For example, if the email stored is "john.doe@oldmail.com"
, the result of the query will return "john.doe@newmail.com"
. This function is useful for dynamically modifying string content during query execution.
5.2.4 New Standard Functions: left()
and right()
Jakarta Persistence 3.2 also introduces the left()
and right()
functions to extract substrings from the left or right side of a string. Here’s an example:
Code Example for left()
TypedQuery<String> leftQuery = emf.createEntityManager().createQuery( "select left(c.phoneNumber, 3) from Customer c where c.id = 7", String.class ); String leftResult = leftQuery.getSingleResult();
This query extracts the first three digits of the phoneNumber
field for the Customer
with an ID of 7
. For example, if the phone number is "9876543210"
, the query will return "987"
.
Code Example for right()
TypedQuery<String> rightQuery = emf.createEntityManager().createQuery( "select right(c.phoneNumber, 4) from Customer c where c.id = 7", String.class ); String rightResult = rightQuery.getSingleResult();
This query extracts the last four digits of the phoneNumber
field for the Customer
with an ID of 7
. For example, if the phone number is "9876543210"
, the query will return "3210"
.
5.3 Improved Sorting
Jakarta Persistence 3.2 introduces enhanced sorting capabilities in JPQL, allowing for more complex order by
clauses. We can now sort by nulls first
or nulls last
, as well as utilize scalar expressions like lower()
or upper()
for case-insensitive sorting. This added flexibility significantly improves the query capabilities of JPQL.
The following example demonstrates how to sort Customer
entities based on their name
field in a case-insensitive manner while placing null
values first. It also sorts by id
in descending order for records with the same name
.
// Insert example data emf.runInTransaction(em -> { em.persist(new Customer(101L, "Alice", "alice@example.com", "123-456-7890")); em.persist(new Customer(102L, "bob", "bob@example.com", "234-567-8901")); em.persist(new Customer(103L, null, "null@example.com", "345-678-9012")); em.persist(new Customer(104L, "Charlie", "charlie@example.com", "456-789-0123")); }); // Perform the sorted query TypedQuery<Customer> query4 = emf.createEntityManager().createQuery( "SELECT c FROM Customer c ORDER BY lower(c.name) ASC NULLS FIRST, c.id DESC", Customer.class ); List<Customer> sortedCustomers = query4.getResultList(); // Display the sorted result sortedCustomers.forEach(customers -> { System.out.println("Customer ID: " + customers.getId() + ", Name: " + customers.getName() + ", Email: " + customers.getEmail() + ", Phone Number: " + customers.getPhoneNumber()); });
6. New Type-Safe Options for Entity Operations
In Jakarta Persistence 3.2, the introduction of marker interfaces like FindOption
, LockOption
, and RefreshOption
provides a type-safe and more readable approach to specifying options for common entity operations such as find()
, lock()
, and refresh()
. This enhancement replaces the previous, error-prone method of using string-based hints, which involved passing a Map
with key-value pairs.
The older approach was verbose, prone to errors due to typos in strings like jakarta.persistence.cache.retrieveMode
, and difficult to read. With the new type-safe options, we can now specify operations in a more concise and clear manner. For example, instead of using a Map
to configure a find()
operation, as was done before:
var customer = em.find(Customer.class, 101L, Map.of("jakarta.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS, "jakarta.persistence.query.timeout", 500, "org.hibernate.readOnly", true));
In Jakarta Persistence 3.2, the same functionality can be achieved as follows:
var customer = em.find(Customer.class, 101L, CacheRetrieveMode.BYPASS, Timeout.milliseconds(500), READ_ONLY);
This approach ensures type safety, improving both the clarity and maintainability of the code while reducing the likelihood of runtime errors.
7. Conclusion
In this article, we explored some new features introduced in Jakarta Persistence 3.2, highlighting improvements in transaction management, JPQL syntax, and enhanced sorting capabilities. We demonstrated how the addition of methods like runInTransaction
and callInTransaction
simplifies transaction handling by automatically managing transaction boundaries. We also showcased how JPQL now supports more flexible query syntax, such as streamlined queries for single entities, new functions like cast()
, replace()
, and left()
, and enhanced sorting options, including case-insensitive sorting and better handling of null values.
8. Download the Source Code
This article explores Jakarta Persistence 3.2, highlighting its features and enhancements.
You can download the full source code of this example here: Jakarta persistence 3.2