Tobias Erdle's Blog

Development Engineer working with different technologies. Former Jakarta MVC & Eclipse Krazo Committer. All views are my own.

Github: erdlet | E-Mail: blog (at) erdlet (punkt) de

Modern web applications with Jakarta EE and Tomcat: Part 4 - Setting up Jakarta Persistence

This article is the fourth part of the series "Modern web applications with Jakarta EE and Tomcat" and demonstrates setting up Jakarta Persistence (JPA). Initially I planned to introduce Flyway as tool for database migrations, but while writing this article I recognized how much work this adds. I'll write a separate article for Flyway.

Topics covered during this article series

Jakarta Persistence (JPA) in a nutshell

The Jakarta Persistence specification defines a standard for managing persistence and object-relational mapping in the Java environment. It uses mainly Annotations for configuration (XML is possible but won't be covered here) and has, in version 3.0, three compatible implementations. Those are namely EclipseLink 3.0.0, Hibernate ORM 5.5.0.Final and Hibernate ORM 6.0.0.Final.

The core mapping concept is called an Entity, which is a domain object implemented as Java Bean. An Entity is declared by adding the @Entity annotation to the non-final class which has also a public or protected default constructor. Other limitations are:

Each Entity has a primary key which can be a single attribute marked by @Id or a composite key. Composite keys won't be covered in this article as they need more explanation.

Relationships between entities can be modeled unidirectional (only owning side) or bidirectional (owning and inverse / not-owning side). The owning side of the relationship determines the update behavior to the database relations. Attention: This is a really important topic a lot of people underestimate. Please understand those relationships before you create complex data models on it. Errors here can cause heavy performance issues.

JPA defines the following relationship types:

Last but not least a few words on the equals and hashCode methods of JPA entities: it is essential to overwrite them correct in terms of JPA. This means, the methods mustn't rely on identifiers which are not existing since the initial creation of the entity, as their result has to be consistent over the whole JPA entity lifecycle. Therefore to avoid weird problems through those methods, you can follow these hints:

Now that the concepts on model side are explained, the technical side is missing.

JPA entities have a Lifecycle which is managed by the EntityManager. The EntityManager is the core interface for interacting with entities. An EntityManager is produced by the EntityManagerFactory which can be instantiated manuall or via XML configuration. This article will show both possibilities.

Integrating Jakarta Persistence (JPA)

Let's start with the integration of JPA into the existing example application. The next steps will guide through adding the required dependencies, preparing the domain model, setting up the database connection in XML or programmatically, handling transactions and finally verifying everything works.

Adding the necessary dependencies

The first step to integrate JPA into the application is to add the necessary dependencies in Maven. Therefore the pom.xml will be extended with the following artifacts:

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

<dependency>
    <groupId>org.hibernate.orm</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.1.1.Final</version>
</dependency>

<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>3.0.0</version>
</dependency>

Those artifacts will provide the Jakarta Persistence API, Hibernate as an implementation and JAXB for being able to handle XML files. This article chooses Hibernate as it is, from the authors view, the most common used implementation and well documented.

Annotate the required classes

After the API is available in the application, the domain models has to be annotated so JPA can handle it. The model's relationship is a 1:n relation between the Post and its Comments. This means, the @OneToMany and @ManyToOne annotations need to be used to express the bidirectional relationship, as well as @Id needs to be used for mapping the primary keys of both entities. To avoid getting to complex, this example won't cover more JPA annotations and concepts.

After the models got their annotations, the Post entity looks like the following snippet. It's declared as an @Entity now, contains an application-generated identifier marked with @Id and the @OneToMany mapping to its Comments. Additionally equals and hashCode are overwritten properly.

import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

@Entity
public class Post {

    @Id
    private UUID id;

    private String title;

    private String content;

    private LocalDateTime publishedAt;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments;

    /**
     * Default constructor for framework usage
     */
    protected Post() {
    }


    // ...

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final Post post = (Post) o;
        return Objects.equals(id, post.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }

    // ...
}

And that's how the Comment entity looks after adding JPA annotations to it:

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;

@Entity
public class Comment {

    @Id
    private UUID id;

    private String author;

    private String content;

    private LocalDateTime publishedAt;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;

    /**
     * Default constructor for framework usage
     */
    protected Comment() {
    }

    // ...

    @Override
    public boolean equals(final Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final Comment group = (Comment) o;
        return Objects.equals(id, group.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }

    // ...
}

Like the Post it's now declared as an @Entity, contains the identifier mapping and has overwritten equals and hashCode methods. Because a bidirectional relationship is implemented, the post attribute is annotated with @ManyToOne and the FetchType.LAZY to avoid peformance issues.

The next step to cover will be to set up an in-memory database and the EntityManager to be able to interact with the API.

Setting up an in-memory database

To avoid the necessarity of installing a complete database or setting up a container, an in-memory database will be used in this example. The database of choice is the H2 database. Because as it's written in Java, it an can be easily embedded in the example application just by adding it as an dependency:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
</dependency>

To use it in in-memory mode, the JDBC connection URI jdbc:h2:mem:modern-webapps will be used.

Option 1: Setting up the EntityManagerFactory with XML

The first approach to configure the JPA EntityManagerFactory is to use the persistence.xml. The persistence.xml contains all settings regarding the datasource, schema-generation and several implementation specific properties. The example persistence.xml looks like this:

<persistence xmlns="https://jakarta.ee/xml/ns/persistence" 
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.0"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
    <persistence-unit name="blog">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>

        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:blog" />
            <property name="jakarta.persistence.jdbc.user" value="sa" />
            <property name="jakarta.persistence.jdbc.password" value="" />

            <!-- Let JPA create the DB schema - DO NOT USE IN PRODUCTION -->
            <property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create" />

            <!-- Configure Hibernate Dialect for SQL generation -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Each application can contain multiple persistence units which all have a unique name. This persistence unit is called blog, which is declared in <persistence-unit name="blog">. As Hibernate is the implementation chosen for this example, the HibernatePersistenceProvider is set. By setting exclude-unlisted-classes to false it is ensured, that all annotated classes are loaded. As an alternative it is possible to declare all entities inside the persistence.xml and load only them. Inside the properties tag the datasource is set up, the schema-generation configured to drop-and-create and the Hibernate dialect for SQL generation set. Attention: Don't use the schema-generation type drop-and-create in other than development or example environments. This setting will drop the whole schema an recreate it from the entities metadata. You can create generation scripts by this, but this is another topic not covered here.

Please be aware of the fact, that setting the JDBC config like this leads to the setup of some default datasource which might be not suitable for production use-cases. Therefore other datasource / database pool implementations exist, like e. g. Apache Commons DBCP or Hikari CP.

Now as the plain config exists, JPA needs to be bootstraped and started on application startup. Therefore a CDI producer and a ServletContextListener is going to be implemented. Before the ServletContextListener can be used, the Jakarta Servlet API needs to be added as provided dependency.

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>5.0.0</version>
    <scope>provided</scope>
</dependency>

The first class to implement is the EntityManagerProducer which will produce EntityManager instances managed by CDI. As @PostConstruct is invoked on first time the @ApplicationScoped bean is used and the application shall not fail only then, the EntityManagerProducer contains a public init() method which will be invoked inside the ServletContextListener on application startup. As the listing demonstrates, with the help of the persistence units name declared in the persistence.xml calling Persistence.createEntityManagerFactory is the only step do perform for setting up the EntityManagerFactory.

After the setup of EntityManagerFactory is performed, the producer method entityManager() can be used by CDI and produce @RequestScoped instances of EntityManager. Because after usage every EntityManager needs to be closed, the closeEntityManager() method peforms this operation before CDI removes the bean. As a last step, at the shutdown of the @ApplicationScoped context, which is usually the application shutdown, the EntityManagerFactory is going to be closed in the @PreDestroy annotated method.

import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;

@ApplicationScoped
public class EntityManagerProducer {

    private static final String PERSISTENCE_UNIT = "blog";

    private EntityManagerFactory emf;

    public void init() {
        emf = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT);
    }

    @Produces
    @RequestScoped
    public EntityManager entityManager() {
        return emf.createEntityManager();
    }

    public void closeEntityManager(@Disposes EntityManager em) {
        em.close();
    }

    @PreDestroy
    void closeEntityManagerFactory() {
        emf.close();
    }
}

Having implemented the producer for the EntityManager, the next step is to initialize the EntityManagerFactory on application startup. Therefore a simple ServletContextListener like shown below is implemented.

import jakarta.inject.Inject;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class WebappInitializer implements ServletContextListener {

    @Inject
    EntityManagerProducer entityManagerProducer;

    @Override
    public void contextInitialized(final ServletContextEvent sce) {
        entityManagerProducer.init();
    }
}

If all these steps are done correct, the application is now able to obtain EntityManagers and database access is possible. The next section will show how to do the steps above without the persistence XML by replacing it with a programatical solution.

Option 2: Setting up the EntityManagerFactory programmatically

As an alternative to the configuration via the persistence.xml file, with the help of some JPA SPI classes a programmatical configuration is possible. To avoid the persistence.xml, the first step is to implement the jakarta.persistence.spi.PersistenceUnitInfo interface. It contains a few getter methods which represent the XML elements from persistence.xml. The following snippet shows the implementation used in the example application.

import jakarta.persistence.SharedCacheMode;
import jakarta.persistence.ValidationMode;
import jakarta.persistence.spi.ClassTransformer;
import jakarta.persistence.spi.PersistenceUnitInfo;
import jakarta.persistence.spi.PersistenceUnitTransactionType;
import org.hibernate.jpa.HibernatePersistenceProvider;

import javax.sql.DataSource;
import java.net.URL;
import java.util.List;
import java.util.Properties;

public class CustomPersistenceUnitInfo implements PersistenceUnitInfo {

    private static final String JPA_VERSION = "3.0";
    private static final PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL;

    private final String persistenceUnitName;
    private final Properties properties;
    private final List<String> managedClasses;

    public CustomPersistenceUnitInfo(
            final String persistenceUnitName,
            final Properties properties, final List<String> managedClasses) {
        this.persistenceUnitName = persistenceUnitName;
        this.properties = properties;
        this.managedClasses = managedClasses;
    }

    @Override
    public String getPersistenceUnitName() {
        return persistenceUnitName;
    }

    @Override
    public String getPersistenceProviderClassName() {
        return HibernatePersistenceProvider.class.getName();
    }

    @Override
    public PersistenceUnitTransactionType getTransactionType() {
        return transactionType;
    }

    @Override
    public DataSource getJtaDataSource() {
        return null;
    }

    @Override
    public DataSource getNonJtaDataSource() {
        return null;
    }

    @Override
    public List<String> getMappingFileNames() {
        return List.of();
    }

    @Override
    public List<URL> getJarFileUrls() {
        return List.of();
    }

    @Override
    public URL getPersistenceUnitRootUrl() {
        return null;
    }

    @Override
    public List<String> getManagedClassNames() {
        return List.copyOf(managedClasses);
    }

    @Override
    public boolean excludeUnlistedClasses() {
        return false;
    }

    @Override
    public SharedCacheMode getSharedCacheMode() {
        return SharedCacheMode.UNSPECIFIED;
    }

    @Override
    public ValidationMode getValidationMode() {
        return ValidationMode.AUTO;
    }

    public Properties getProperties() {
        return properties;
    }

    @Override
    public String getPersistenceXMLSchemaVersion() {
        return JPA_VERSION;
    }

    @Override
    public ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    @Override
    public void addTransformer(ClassTransformer transformer) {

    }

    @Override
    public ClassLoader getNewTempClassLoader() {
        return null;
    }
}

The class needs to get the name of the persistence unit, in this specific case a non-JTA datasource and the custom properties provided. As the persistence.xml isn't used anymore, the init() method in the EntityManagerProducer has to be changed.

  public void init() {
        final var properties = new Properties();
        properties.put("jakarta.persistence.jdbc.url", "jdbc:h2:mem:blog");
        properties.put("jakarta.persistence.jdbc.driver", Driver.class.getName());
        properties.put("jakarta.persistence.jdbc.user", "sa");
        properties.put("jakarta.persistence.jdbc.password", "");
        properties.put("hibernate.dialect", H2Dialect.class.getName());
        properties.put("jakarta.persistence.schema-generation.database.action", "drop-and-create");
        properties.put("hibernate.show_sql", true);

        final var persistenceUnitInfo =
                new CustomPersistenceUnitInfo(PERSISTENCE_UNIT, properties, getEntities());

        emf = new HibernatePersistenceProvider().createContainerEntityManagerFactory(
                persistenceUnitInfo, Map.of());
    }

    private List<String> getEntities() {
        return Stream.of(Post.class, Comment.class).map(Class::getName).toList();
    }

In contrast to the setup with the persistence.xml file, this setup needs to set the managed classes manually. This could be done also by some class loader logic, checking classes for the @Entity annotation and so on, but this is too advanced for this article.

In constrast to the configuration with XML, this approach is more flexible, as several configuration sources can be used. Instead of the hard-coded values used in this example, one can e.g. use files or environment variables for configuration.

Transaction management without JTA

Because the example is running outside an Jakarta EE compliant application server, JTA and it's possibility to use declarative transaction management is not available. Maybe this will be covered in a follow up article. Anyway there are other possibilities to work with transactions in JTA without annotations. Here are two common options shown and short explained.

EntityManager.getTransaction()

The easiest approach is to use the EntityManager#getTransaction method, which contains methods like begin(), commit() or rollback(). As an example, the PostService#create method was refactored to use JPA and looks like this now:

public Post create(final PostDTO dto) {
        final var post = new Post(dto.getTitle(), dto.getContent(), LocalDateTime.now());

        try {
            em.getTransaction().begin();

            em.persist(post);

            em.getTransaction().commit();

            return post;
        } catch (Exception ex) {
            em.getTransaction().rollback();

            throw ex;
        }
    }

Apache DeltaSpike

The Apache DeltaSpike JPA module provides support for declarative transaction support. Unfortunately, DeltaSpike doesn't support the jakarta.* namespace yet, so it won't be used within the example application. Anyway, there is a work in progress issue which targets the namespace update.

Verify database interaction manually

Finally after all configuration work is done the application can be tested. Again it's done manually by calling the GET /blogging-app/posts and POST /blogging-app/posts resource. It is expected that the first GET /blogging-app/posts returns an empty result, as there are no records in the database. After calling POST /blogging-app/posts at least one time, there shall be records returned after requesting GET /blogging-app/posts again.

GET http://localhost:8080/blogging-app/posts

HTTP/1.1 200 
Content-Type: text/plain
Content-Length: 0
Date: Thu, 28 Jul 2022 11:57:49 GMT
Keep-Alive: timeout=20
Connection: keep-alive

<Response body is empty>

###########################################################
POST http://localhost:8080/blogging-app/posts
Content-Type: application/x-www-form-urlencoded

content=MySecondContent&title=How to do more posts

...

###########################################################

GET http://localhost:8080/blogging-app/posts

HTTP/1.1 200 
Content-Type: text/plain
Content-Length: 424
Date: Thu, 28 Jul 2022 11:59:43 GMT
Keep-Alive: timeout=20
Connection: keep-alive

Post{id=7f6f146d-03a9-4e53-8825-255974cdc3be, title='How to do more posts', content='MySecondContent', publishedAt=2022-07-28T13:59:17.918803}, 
Post{id=53413eb5-f3f0-46e5-9b3e-b36f8b2d5590, title='How to do more posts', content='MySecondContent', publishedAt=2022-07-28T13:58:27.330675}, 
Post{id=c2bc820f-b9d6-4b55-b596-143095d896cb, title='Title from POST', content='MyFirstContent', publishedAt=2022-07-28T13:58:11.820470}

To ensure that really JPA did the work, a look into the server log shows the queries generated.

Hibernate: select p1_0.id,p1_0.content,p1_0.publishedAt,p1_0.title from Post p1_0 order by p1_0.publishedAt desc
Hibernate: insert into Post (content, publishedAt, title, id) values (?, ?, ?, ?)
Hibernate: insert into Post (content, publishedAt, title, id) values (?, ?, ?, ?)
Hibernate: select p1_0.id,p1_0.content,p1_0.publishedAt,p1_0.title from Post p1_0 order by p1_0.publishedAt desc
Hibernate: insert into Post (content, publishedAt, title, id) values (?, ?, ?, ?)
Hibernate: select p1_0.id,p1_0.content,p1_0.publishedAt,p1_0.title from Post p1_0 order by p1_0.publishedAt desc

Those responses and logs show that JPA and Hibernate are doing their job and persist / read data from the database.

Conclusion

This article shows that JPA and Hibernate are powerful, but at the same time very complex tools that need to be used wisely. For this purpose, the principles of JPA were first explained and then the integration and configuration in an application running on Tomcat was shown. In addition, the topic of "transaction management" was briefly considered. Finally, the integration was tested manually to see if it works.

Upcoming topic: Setting up Jakarta MVC for server-side rendered frontends

The next article will focus on how to develop a Jakarta REST based frontend using Jakarta MVC.

Resources