Skip to main content

This guide shows you how to store and retrieve data using Jakarta Persistence.

We will first begin by summarizing what we want to build. Next, we build a RESTful web service that can consume data, store it in a database using Jakarta Persistence, and serve it as a REST endpoint. For those unfamiliar with RESTful web services, we recommend reading our previous article.

We will build an application that handles coffee product data. The service will consume a JSON payload containing the product’s ID, name, and price. Here’s an example of the JSON payload:

{
  "id": 1,
  "name": "Coffee-A",
  "price": "2.75"
}

The application will store the data and provide REST endpoints for basic CRUD (Create, Read, Update, and Delete) operations. We can then further customize and improve it according to our needs.

OK, now that we have specified our requirements, you will need to follow these steps:

Set up your development environment:

  • Install a Java Development Kit (JDK). Please make sure you have Java SE 11 or higher (we have tested with Java SE 11 and Java SE 17). You can choose any vendor distribution of your choice as well as from Adoptium.
  • Install an application server that supports Jakarta EE. Download any of the Jakarta EE compatible products.
  • Install Maven 3 or higher.

To install JDK and Maven, we can use the SDKMan. We can go through the steps mentioned in the how-to guide.

How to complete this guide

In this getting started guide, you may use Eclipse Starter for Jakarta EE, finish each step, or skip fundamental setup stages you already know. You can also begin with an IDE or choose a project structure from well-known Maven archetypes.

Create a new project with Eclipse Starter for Jakarta EE

To use Eclipse Starter for Jakarta EE, we need to take the following steps:

  1. Navigate to https://start.jakarta.ee. This service will set up all the essential dependencies for an application. The current version of the Starter only supports Maven. In the future, we may be able to choose between Gradle and Maven.
    A screenshot of the Eclipse Starter for jakarta EE form with fields selected
  2. Select the desired version of Jakarta EE from the available options. Currently, the options include Jakarta EE 8, Jakarta EE 9.1, and Jakarta EE 10. In addition, you may choose the Jakarta EE Platform or one of the Jakarta EE profiles (Web, Core).
  3. For this project, we have chosen Jakarta EE 10 Platform, Java SE 11 and WildFly as a Runtime.
  4. Once we have selected our desired options, we will click the generate button. This will give us the project structure and sample code, which we can then build and run.

Let’s explore the code structure

When we unpack the generated code, we will have a structure of an application. We can open it in our favourite IDE, and then we can just run it from the command line.

.
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    └── main
        ├── java
        │   └── org
        │       └── eclipse
        │           └── jakarta
        │               └── hello
        │                   ├── Hello.java
        │                   └── HelloWorldResource.java
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            ├── images
            │   └── jakartaee_logo.jpg
            └── index.html

This generated source code comes with an embedded Maven wrapper. So if you want to use it, make sure you run the following command first for Unix environments (Linux or Mac):

$ chmod +x mvnw

Since we are using WildFly as a runtime, the following command would run the application:

$ ./mvnw clean package wildfly:run

On the other hand, if you have Maven installed, you can run the following command:

$ mvn clean package wildfly:run

For Windows, we don’t need to run chmod command; rather use mvnw.cmd instead of mvnw.

Now, if you hit the browser with the following URL, you will see the result.

http://localhost:8080/jakartaee-hello-world

We have already covered how to test them out in our previous article, so we’re skipping it.

Setting up the Jakarta Persistence

Since our goal is to start with Jakarta Persistence, let’s first understand what Jakarta Persistence essentially is. Java Persistence API is a Java programming language specification that provides an object-relational mapping (ORM) framework for managing relational data in Java applications. It simplifies database access and enables developers to work with objects and classes rather than SQL statements. It was formally known as Java Persistence API, in short, JPA.

The next step is we start by creating our first Entity. An entity is a Java class that corresponds to a table of a database. This article will use an embedded H2 database to store our data.

Create a Coffee class in the org.eclipse.jakarta.model.entity package.

package org.eclipse.jakarta.model.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.io.Serializable;

@Entity
public class Coffee implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String  price;

    //getter and setter    

 }

As you can see, our Entity Class contains several annotations. The @Entity annotation designates a Java class as an Entity class, representing a database table where the class fields correspond to table columns. The @Id annotation marks a field or property in the entity class as the primary key. Additionally, you can use the @GeneratedValue annotation alongside @Id to indicate that the primary key value should be generated automatically.

The primary idea behind the Jakarta Persistence is to allow Java programmers to work with objects. They can create instances of these entities, and Jakarta Persistence will convert them into data and store them in the database. When an object is needed, Jakarta Persistence retrieves data and converts it into an object for easy use.

How to connect the Entity to a DataSource?

Before using Jakarta Persistence, the first step is configuring the database connection by creating a Jakarta Persistence configuration file named persistence.xml. This file can be placed under the resources/META-INF folder in our project.

The persistence.xml file allows specifying the JDBC Connection Settings or the Datasource JNDI1 name. For instance, we can use the WildFly default H2 database, specified within the persistence.xml.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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_1.xsd"
             version="3.0">

    <persistence-unit name="coffees">
        <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
        <properties>
            <property
                    name="jakarta.persistence.schema-generation.database.action"
                    value="drop-and-create" />
            <property name="jakarta.persistence.sql-load-script-source"
                      value="META-INF/initial-data.sql" />
            <property name="eclipselink.logging.level.sql" value="FINE" />
            <property name="eclipselink.logging.parameters" value="true" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

So create a persistence.xml file in resources/META-INF folder and copy the above.

This persistence.xml file sets up a persistence unit named "coffees". It specifies the JNDI name of the JDBC datasource to be used for database connection management, which is "java:jboss/datasources/ExampleDS" by default in WildFly for an H2 database. The configuration for JNDI name is usually located in the standalone.xml file in the WILDFLY_HOME/standalone/configuration directory, where WILDFLY_HOME is the installation directory of WildFly. However, no additional configuration is required for this example since we are using the default JNDI name. Other runtimes may have slightly different configurations, which can be found in their documentation.

The element contains several properties that configure the behaviour of the Jakarta Persistence provider. For example, the "jakarta.persistence.schema-generation.database.action" property in the persistence.xml file specifies the action to be taken by the Jakarta Persistence provider when generating the database schema. Some of the other options are:

  • none: The Jakarta Persistence provider won’t generate the database schema.
  • create: The Jakarta Persistence provider will create the database schema.
  • drop: The Jakarta Persistence provider will drop the database schema.
  • drop-and-create: The Jakarta Persistence provider will drop the existing database schema and create a new one.

There are other options available for this property as well, such as "create-only", "drop-and-create-script", and "create-script". The choice of the option depends on the use case and the application’s specific requirements. For this particular case, "drop-and-create" option is chosen in the persistence.xml file, which drops the existing database schema and creates a new one whenever the application runs in the development environment.

The "jakarta.persistence.sql-load-script-source" property specifies the location of the SQL script to be executed when the persistence unit is initialized. Here, the script is located in the META-INF/initial-data.sql file. So create a file named initial-data.sql and put the following insert queries in it.

INSERT INTO coffee (name, price) VALUES ('Coffee-A', 2.75);
INSERT INTO coffee (name, price) VALUES ('Coffee-B', 1.99);
INSERT INTO coffee (name, price) VALUES ('Coffee-C', 3.25);
INSERT INTO coffee (name, price) VALUES ('Coffee-D', 2.99);

The purpose of this script is to ensure that when the server starts up, there is some data present in the database which can be used for testing or demonstration purposes.

Other properties are used to configure logging for the Jakarta Persistence provider, such as "eclipselink.logging.level.sql" and "eclipselink.logging.parameters". Finally, the "hibernate.show_sql" property is used to enable SQL query logging for the Hibernate Jakarta Persistence provider2 .

Setting up the Jakarta Persistence repository

We will now create a CafeRepository class that will be responsible for handling the Create, Read, Update, and Delete (CRUD) operations on the Coffee entity.

package org.eclipse.jakarta.model;

import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.eclipse.jakarta.model.entity.Coffee;

import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Optional;
import java.util.logging.Logger;

@Stateless
public class CafeRepository {
    private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    @PersistenceContext
    private EntityManager em;

    public Coffee create(Coffee coffee) {
        logger.info("Creating coffee " + coffee.getName());
        em.persist(coffee);

        return coffee;
    }

    public List<Coffee> findAll() {
        logger.info("Getting all coffee");
        return em.createQuery("SELECT c FROM Coffee c", Coffee.class).getResultList();
    }

    public Optional<Coffee> findById(Long id) {
        logger.info("Getting coffee by id " + id);
        return Optional.ofNullable(em.find(Coffee.class, id));
    }

    public void delete(Long id) {
        logger.info("Deleting coffee by id " + id);
        var coffee = findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Invalid coffee Id:" + id));
        em.remove(coffee);
    }

    public Coffee update(Coffee coffee) {
        logger.info("Updating coffee " + coffee.getName());
        return em.merge(coffee);
    }
}

The class is annotated with @Stateless, which makes it a Stateless session bean. A Stateless session bean is a type of Jakarta Enterprise Beans (EJB) that is used for implementing business logic in Jakarta EE applications. Stateless session beans are designed for scenarios where the bean does not need to maintain any conversational state with the client between method invocations. In other words, a Stateless session bean doesn’t remember any client-specific data between method calls.

Let’s break it down step-by-step:

EntityManager:

The EntityManager is the primary interface for managing entities in Jakarta Persistence. It is annotated with @PersistenceContext, which automatically injects an instance of the EntityManager into the class.

create():

This method takes a Coffee object as a parameter, logs an informational message, and persists the object using the EntityManager. The persisted Coffee object is returned.

findAll():

This method retrieves all the Coffee entities from the database. It creates a Jakarta Persistence query, executes it, and returns a list of Coffee objects.

findById():

This method takes a Long id as a parameter, logs an informational message, and searches for the Coffee entity with the specified id using the EntityManager. If found, it returns an Optional<Coffee> containing the entity; otherwise, it returns an empty Optional.

delete():

This method takes a Long id as a parameter, logs an informational message, and tries to find the Coffee entity with the specified id. If found, it removes the entity using the EntityManager. If the entity is not found, an IllegalArgumentException is thrown with a message indicating the invalid id.

update():

This method takes a Coffee object as a parameter, logs an informational message, and updates the existing Coffee entity in the database using the EntityManager. The updated Coffee object is returned.

Adding REST Endpoints for the CRUD methods

Finally, we will add a REST Endpoint to be able to access our Coffee Service from a remote REST Client.

package org.eclipse.jakarta.rest;

import jakarta.inject.Inject;
import jakarta.persistence.PersistenceException;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import org.eclipse.jakarta.model.CafeRepository;
import org.eclipse.jakarta.model.entity.Coffee;

import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.logging.Logger;

@Path("coffees")
public class CafeResource {
    private final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());

    @Inject
    private CafeRepository cafeRepository;

    @GET
    @Path("{id}")
    @Produces("application/json")
    public Coffee findCoffee(@PathParam("id") Long id) {
        logger.info("Getting coffee by id " + id);
        return cafeRepository.findById(id)
            .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
    }

    @GET
    @Produces("application/json")
    public List<Coffee> findAll() {
        logger.info("Getting all coffee");
        return cafeRepository.findAll();
    }

    @POST
    @Consumes("application/json")
    @Produces("application/json")
    public Coffee create(Coffee coffee) {
        logger.info("Creating coffee " + coffee.getName());
        try{
            return cafeRepository.create(coffee);
        }catch (PersistenceException ex){
            logger.info("Error creating coffee " + coffee.getName());
            throw new WebApplicationException(Response.Status.BAD_REQUEST);
        }
    }

    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") Long id) {
        logger.info("Deleting coffee by id " + id);
        try{
            cafeRepository.delete(id);
        }catch (IllegalArgumentException e){
            logger.info("Error deleting coffee by id " + id);
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
    }


    @PUT
    @Consumes("application/json")
    @Produces("application/json")
    public Coffee update(Coffee coffee) {
        logger.info("Updating coffee " + coffee.getName());
        try{
            return cafeRepository.create(coffee);
        }catch (PersistenceException ex){
            logger.info("Error updating coffee " + coffee.getName());
            throw new WebApplicationException(Response.Status.BAD_REQUEST);
        }
    }
}

The CafeResource class is a RESTful web service for managing Coffee entities using the Jakarta RESTful Web Services. The class exposes various HTTP endpoints for performing CRUD operations, such as creating, retrieving, updating, and deleting Coffee objects.

  1. @Path("coffees"): This annotation sets the base path of the web service to "/coffees". All the HTTP endpoints in this class will be relative to this path.
  2. @Inject: This annotation is used to inject an instance of the CafeRepository class, which handles the persistence operations for Coffee entities.
  3. @GET, @POST, @PUT, @DELETE: These annotations define the HTTP methods for the corresponding methods in the class (e.g., findCoffee(), findAll(), create(), delete(), and update()). They map the Java methods to HTTP operations.
  4. @Path("{id}"): This annotation is used in combination with the @GET and @DELETE annotations to specify the path parameter “id” for retrieving or deleting a Coffee entity by its id.
  5. @Produces and @Consumes: These annotations are used to define the media type (e.g., "application/json") that a method can produce as a response or consume as input. In this case, the methods accept and return JSON representations of Coffee entities.

The class also utilizes the Logger for logging informational messages and handles exceptions by throwing WebApplicationException with appropriate HTTP status codes (e.g., Response.Status.NOT_FOUND or Response.Status.BAD_REQUEST) in case of errors.

Let’s test the Service

After adding the aforementioned classes to your project, execute the following command to build and run the application:

$ mvn clean package wildfly:run

This command will clean any previous builds, compile and package the application, and then deploy it to the WildFly application server.

If everything is set up correctly, the application will be accessible through a web browser or a REST client. You can use cURL as a command-line REST client to interact with the service.

Here are the cURL commands for the different CRUD operations in the CafeResource class, using the provided base URL and JSON payload:

Create a new Coffee entity (HTTP POST):

curl -X POST \
 http://localhost:8080/jakartaee-hello-world/rest/coffees \
 -H 'Content-Type: application/json' \
 -d '{
 "id": 1,
 "name": "Coffee-A",
 "price": "2.75"
}'

Retrieve all Coffee entities (HTTP GET):

curl -X GET \
 http://localhost:8080/jakartaee-hello-world/rest/coffees \
 -H 'Content-Type: application/json'

Retrieve a specific Coffee entity by id (HTTP GET):

curl -X GET \
 http://localhost:8080/jakartaee-hello-world/rest/coffees/<id> \
 -H 'Content-Type: application/json'

Update an existing Coffee entity (HTTP PUT):

curl -X PUT \
 http://localhost:8080/jakartaee-hello-world/rest/coffees \
 -H 'Content-Type: application/json' \
 -d '{
 "id": 1,
 "name": "Coffee-A",
 "price": "2.75"
}'

Delete a Coffee entity by id (HTTP DELETE):

curl -X DELETE \
 http://localhost:8080/jakartaee-hello-world/rest/coffees/<id> \
 -H 'Content-Type: application/json'

Make sure to replace the placeholder with the actual id value when using the cURL commands for retrieving or deleting a specific Coffee entity.

Conclusion

Congratulations! You have just learned how to develop a web service using Jakarta Persistence.


  1. The Java Naming and Directory Interface (JNDI) is a Java API for a directory service that allows Java software clients to discover and look up data and objects via a name.
  2. Jakarta Persistence is a Java ORM specification with multiple compatible implementations, such as EclipseLink 3.0.0 and Hibernate ORM 6.0.0.Final.

Back to the top