Jakarta Persistence 3.1 new features

This tutorial introduces Jakarta Persistence API 3.1 as a standard for management of persistence and O/R mapping in Java environments. We will discuss the headlines with a simple example that you can test on a Jakarta EE 10 runtime.

New features added in Jakarta Persistence 3.1

There are several new features available in Jakarta Persistence 3.1. We can group them in two main blocks:

  • How to use java.util.UUID as auto-generated Primary Key
  • How to enhance your JPQL with new Math, Date and Time Functions have been added to the Query language and the Criteria API.

To show these features you will need a Jakarta EE 10 runtime. For the purpose of this tutorial, we will be using WildFly 27 Preview which is available for download at: https://www.wildfly.org/downloads/

Coding a JPA / REST application

Firstly, you need to put in place the Jakarta EE 10 dependency, which is now available in the Maven repository:

<dependencies>
    <dependency>
	  <groupId>jakarta.platform</groupId>
	  <artifactId>jakarta.jakartaee-api</artifactId>
	  <version>10.0.0</version>
	</dependency>
</dependencies>

Next, let’s start coding. We will begin by adding an Entity Class which uses a java.util.UUID as Primary Key.

import java.sql.Date;
import java.util.UUID;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;
    private String model;
    private Double price;
    private Date created;

    // Getters/Setters omitted for brevity

}

In a nutshell the java.util.UUID class represents an immutable universally unique identifier (UUID). It is a common choice when you want to create a random file name, a session id in web application, or a transaction id. It is especially useful in Cloud environments where automatic key generation is not always available for some databases.

When you choose , a random unique identifier will be added as a primary key for the “id” field. This is the 128 bit format of a random java.util.UUID:

"22c5ff94-4c34-49a3-ae4a-2eb39cd4c422"

Managing the Entity

Next, we will add an EJB which contains a reference to the EntityManager. In the following implementation we will add include a method to persist the Entity and two finder methods:

  • findAll is a generic finder that returns a List of all Entity
  • findById which finds by Primary key. In our case, the Primary Key Class is java.util.UUID:
@Stateless
public class  ServiceBean   {

    @PersistenceContext
    private EntityManager em;

    public void save(Car p){
        em.persist(p);
    }
 
    public List<Car> findAll(){
        Query query = em.createQuery("select c FROM Car c");
        List <Car> list = query.getResultList();
        return list;
    }

    public Car findById(UUID id){
        Car p = em.find(Car.class, id);
        return p;
    }

} 

Exposing the Resource

Finally, we will add a REST Service to expose the Service methods of our application. As you can see from the following code example, each REST method is mapping a method from the EJB Service:

@Path("/carservice")
@RequestScoped
public class RestService {

    @Inject
    private ServiceBean ejb;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Car> listAllCars() {
        return ejb.findAll();
    }

    @GET
    @Path("/id/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Car findById(@PathParam("id") String id) {

        Car p = ejb.findById(UUID.fromString(id));
        if (p == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
        return p;
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createCar(Car car) {

        Response.ResponseBuilder builder = null;

        try {

            ejb.save(car);
            builder = Response.ok("Saved!");

        } catch (Exception e) {
            // Handle generic exceptions
            Map<String, String> responseObj = new HashMap<>();
            responseObj.put("error", e.getMessage());
            builder = Response.status(Response.Status.BAD_REQUEST).entity(responseObj);
        }

        return builder.build();
    }


}

For the sake of completeness, we will also add the RestActivator Class, that you need to bootstrap the Jakarta REST Service:

@ApplicationPath("/rest")
public class RestActivator extends Application {
    /* class body intentionally left blank */
}

Testing the application

You can test the application with any tool of your likes. For example, let’s use Postman to save a new Car with a POST:

jakarta persistence 3.1 example

Next, let’s fetch the list of Cars with a GET:

jakarta persistence 3.1 tutorial

As you can see from the above screen-shot, the Entity has a Primary key whose class is java.util.UUID.

Using the EXTRACT function in JPQL

Another cool feature of Jakarta Persistence 3.1 is the addition of the EXTRACT function in the JPQL. This built-in function allows you to extract a numeric part from a given date. For example, the following findExtract method returns a list of tuples where, instead of the full year, we EXTRACT only the YEAR numeric field:

public List<Car> findExtract(){

    Query query = em.createQuery("select id,model, price,  EXTRACT(YEAR FROM created) as y FROM Car c ");
    List <Car> list = query.getResultList();
    return list;

}

And here is a sample output from the above query:

[
"a529e3b2-6a1f-4dc0-8c8b-10c27377b2d6",
"fiat",
15090.1,
2021
]

Using Local Date functions in JPQL

You can now use in JPQL some functions that return the current local date and time using java.time types: LOCAL DATE, LOCAL DATETIME, LOCAL TIME

Here is a proof of concept example which adds all the above functions in our Car tuple:

public List<Car> findLocalTime(){

    Query query = em.createQuery("select id,  LOCAL TIME as localTime, LOCAL DATETIME as localDateTime, LOCAL DATE as localDate FROM Car c ");
    List <Car> list = query.getResultList();
    return list;

}

And this is a sample output from the above Query:

[
    "86df3b6c-f0cd-420e-92ec-b04fca82e293",
    "11:46:07",
    "2022-09-27T11:46:06.665457",
    "2022-09-27"
]

Using Numeric Functions in JPQL

Finally, the JPQL now also includes a set of Numeric functions you can use on top of your fields: CEILING, EXP, FLOOR, LN, POWER, ROUND, SIGN.

An all-in-one example to test the above numeric functions could be the following one:

public List<Car> testNumericFunctions(){

    Query query = em.createQuery("SELECT c.model,"
            + "CEILING(c.price),"
            + "FLOOR(c.price),"
            + "ROUND(c.price, 1),"
            + "EXP(c.price),"
            + "LN(c.price),"
            + "POWER(c.price,2),"
            + "SIGN(c.price)"
            + " FROM Car c");

    List <Car> list = query.getResultList();
    return list;

}

Troubleshooting

In the first round of tests, I have hit the following Exception when running the JPA Query:

java.lang.IllegalArgumentException: org.hibernate.query.sqm.StrictJpaComplianceViolation: Encountered implicit select-clause, but strict JPQL compliance was requested

As you can read from the above exception, JPA now enforces strict JPQL compliance. In particular, we need to specify the select clause explicitly in our Query:

Query query = em.createQuery("select c FROM Car c");

On the other hand, if you want to disable strict JPQL compliance , you can add the following Hibernate property in your persistence.xml:

<property name="hibernate.jpa.compliance.query" value="false"/>

Source code

The source code for this article is available here: https://github.com/fmarchioni/mastertheboss/tree/master/jpa/uuid-demo