Core Java

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: The Address 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, and zipCode.
  • Embedding the Address Record: The address field in Customer is annotated with @Embedded, signifying that the Address record is embedded as part of the Customer 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.

Download
You can download the full source code of this example here: Jakarta persistence 3.2

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button