Modern web applications with Jakarta EE and Tomcat: Part 4 - Setting up Jakarta Persistence
26 Jul 2022 - Tobias ErdleThis 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
- Setting up Jakarta REST with Jersey
- Setting up Jakarta Bean Validation
- Setting up Jakarta Contexts and Dependency Injection
- Setting up Jakarta Persistence (this article)
- Setting up Jakarta MVC for server-side rendered frontends
- Content Negotiation with Jakarta REST
- Unit and integration testing
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:
- Entity must be a top-level class, so no enums, records or interfaces are allowed
- No methods or persistent instance attributes may be final (attributs marked as
transient
are not persistent) - Abstract or concrete classes can be entities.
- For each persistent attribute an accessor method (get / set) must exist
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:
- Bidirectional
@OneToOne
- Bidirectional
@ManyToOne
/@OneToMany
- Unidirectional
@OneToOne
- Unidirectional
@ManyToOne
- Bidirectional
@ManyToMany
- Unidirectional
@OneToMany
- Unidirectional
@ManyToMant
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:
- Identifier is set by database: don't use identifier in
hashCode
and try to avoid it inequals
. SethashCode
to a fix value. - Identifier is set by application: you can use the identifier. Just generate
equals
andhashCode
.
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 Comment
s. 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 Comment
s. 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 EntityManager
s 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.