In a Jakarta Persistence application, all types are sourced from the jakarta.persistence package.
This API is comprehensive, and with Jakarta Persistence 3.1, it includes 16 interfaces (originally, there were only 4: EntityManager, EntityManagerFactory, EntityTransaction, and Query), a Persistence class (relevant only for Java SE applications), 19 enumerations, 11 exception classes, and 91 annotations for entity beans.
The meaning of the annotations can be easily understood by examining the types; specifications are set declaratively and not programmed via an API.
The EntityManager interface is perhaps the most important among all and it offers about 50 methods. All interfaces are implemented by the respective persistence provider, comparable to how a JDBC driver implements the JDBC API.
Getting the EntityManager from Spring
After we’ve programmed the entity bean, we can turn our attention to the API of the EntityManager. This allows us to load, save, and delete the entity bean. We want to read with the EntityManager first, then write later. Transactions are necessary for writing.
Inject EntityManager
The EntityManager represents an open session with the database. Spring Boot builds an EntityManager via auto-configuration, which we can inject directly:
@Autowired EntityManager em;
An alternative possibility offers the annotation @PersistenceContext. This is an annotation from the Jakarta Persistence.
Inject the EntityManager and Test If It Exists
For access via the EntityManager to work, several things must fit together: The database must be running, the connection data must be correct so that a connection can be established, the entity bean class must be found, and the entity bean metadata must match the database schema.
To check if all of this is in order, the demo in the following listing might help.
package com.tutego.date4u.interfaces.shell;
import org.slf4j.LoggerFactory;
import org.springframework.shell.standard.*;
import jakarta.persistence.EntityManager;
import static java.util.Objects.isNull;
@ShellComponent
public class EntityManagerCommands {
private final EntityManager em;
public EntityManagerCommands( EntityManager em ) { this.em = em; }
@ShellMethod( "Display profile" )
public String findprofile( long id ) {
LoggerFactory.getLogger( getClass() ).info( "{}", em );
return isNull( em ) ? "null" : em.getClass().getName();
}
}
If we call findprofile from the shell—the ID is ignored by the code for now—a Class object identifier should appear, but no exception or null should show up on the console.
Note: Working with the EntityManager API? Spring applications can use the Jakarta Persistence API in two ways. First, the EntityManager API can be used directly, and, second, there is the Spring Data project, particularly Spring Data JPA, which works in the background with the EntityManager. While working with Spring Data JPA eventually, we won’t have direct contact with the EntityManager. However, it’s essential to learn the basics of the EntityManager API as it helps us understand the persistence context and how entity beans are managed and monitored in memory. Even if Spring Data JPA makes the EntityManager API obsolete, the familiar annotations and JPQL as a query language remain relevant.
Now let’s look at some core EntityManager methods. You’ll find the Javadoc link at https://jakarta.ee/specifications/persistence.
Search an Entity Bean by Its Key: find(…)
Method find(…) in EntityManager searches for an entity bean using a given key. Its declaration is as follows:
<T> T find(Class<T> entityClass, Object primaryKey)
The first parameter is a type token, which is precisely the type of entity that is to be loaded. The second parameter is the primary key; this is of type Object because the key can be of different types. For example, load the profile with ID 1:
Profile fillmoreFat = em.find( Profile.class, 1L );
In our case, the primary key is 1L, which leads to Long.valueOf(1), a wrapper object. The keys are always objects; this is where autoboxing is helpful. If the data types aren’t correct, for example, because the type token doesn’t belong to any entity bean or the key type is wrong, this leads to an IllegalArgumentException at runtime, that is, an unchecked exception.
If the find(…) method finds nothing, the return is null. This is rather uncommon in modern API design because you would use Optional today; the EntityManager API is much older than Java 8. If you prefer to work with Optional and like the cascading with map(…), you can work with Option.ofNullable(…):
Optional<Profile> maybeProfile =
Optional.ofNullable( em.find(Profile.class, 1L) );
The Optional either contains a reference to the loaded entity bean or is Optional.empty() if there was no profile under the ID.
Task: Find and Output Profiles with ID
We’ve described how we started with the shell method findprofile:
@ShellMethod( "Display profile" )
public String findprofile( long id ) {
LoggerFactory.getLogger( getClass() ).info( "{}", em );
return isNull( em ) ? "null" : em.getClass().getName();
}
The findprofile command should evaluate the parameter so that a profile with the given ID is loaded and displayed.
Proposed solution:
@ShellMethod( "Display profile" )
public String findprofile( long id ) {
return Optional.ofNullable( em.find( Profile.class, id ) )
.map( profile->profile.getNickname()+", "+profile.getManelength() )
.orElse( "Unknown profile for ID " + id );
}
The proposed solution uses the previously discussed method of using Optional.ofNullable(…) to convert the reference to an Optional. The map(…) method transforms the Optional<Profile> to an Optional<String> with the nickname and mane length. Because our shell method must always return a string, orElse(…) ensures that the string "Unknown profile for ID " + id appears at the end in case the find(…) method failed to load the entity and returned null, resulting in Optional.empty(). If we load the profile with ID 1, the following SQL is generated and displayed:
select
p1_0.id,
p1_0.attracted_to_gender,
p1_0.birthdate,
p1_0.description,
p1_0.gender,
p1_0.lastseen,
p1_0.manelength,
p1_0.nickname
from
profile p1_0
where
p1_0.id=?
Displaying Tables with Spring Shell
The output with the string is simple and can be enhanced. The Spring Shell project provides a way to draw tables. The method to transform a Profile object into a string with a table representation looks like this:
private String formatProfileAsTable( Profile p ) {
TableModel tableModel = new TableModelBuilder<String>()
.addRow().addValue( "ID" ).addValue( "" + p.getId() )
.addRow().addValue( "Mane length" ).addValue( "" + p.getManelength() )
.addRow().addValue( "Nickname" ).addValue( p.getNickname() )
.addRow().addValue( "Birthdate" ).addValue( p.getBirthdate().toString() )
.build();
Table table =
new TableBuilder( tableModel )
.addFullBorder( BorderStyle.fancy_light ).build();
return table.render( 100 );
}
Spring Shell type TableModelBuilder allows the construction of TableModel objects, which then become a Table. This can be rendered into a string in the next step.
In TableModelBuilder, method addRow() introduces a new row, and new columns are added by addValue(…). Our code creates four columns. Finally, the build() method returns a TableModel object, which is passed to TableBuilder. A border style is set, and build() returns the Table object, which renders(100) to the width of 100 characters in a String.
TableBuilder can do a little more. For example, headers can be added, and different border styles can be used. Method formatProfileAsTable(…) can be used with map(…):
@ShellMethod( "Display profile" )
public String findprofile( long id ) {
return Optional.ofNullable( em.find( Profile.class, id ) )
.map( this::formatProfileAsTable )
.orElse( "Unknown profile for ID " + id );
}
find(…) and getReference(…)
Besides the find(…) method, there is a method with identical parameter types that sound similar in name—getReference():
<T> T getReference(Class<T> entityClass, Object primaryKey)
The key difference between getReference(…) and find(…) is that the former doesn’t return a fully populated object. Instead, it returns a proxy, without triggering an SQL query by the Jakarta Persistence provider. The proxy employs lazy loading and waits until the first access to fill the entity bean.
The container remains unfilled until the proxy is accessed, and the Jakarta Persistence provider intercepts the method calls in the process. Thus, directly querying the instance variables doesn’t trigger loading. The following code demonstrates this behavior:
Profile fillmoreFat = em.getReference( Profile.class, 1L );
System.out.println( fillmoreFat.getClass() );
System.out.println( fillmoreFat.getNickname() );
System.out.println( fillmoreFat.getLastseen() );
The Class object indicates the proxy type, and getNickname() triggers loading.
The getReference(…) method has two peculiarities. You could also say it has two weaknesses:
- If the entity doesn’t exist, an EntityNotFoundException follows only later on the first access.
- The proxy is just an empty container at the beginning. There is no loading process until the proxy is accessed. Because the EntityManager can’t know when code accesses the proxy, the database connection could have ended long ago. The result with Hibernate is an org.hibernate.LazyInitializationException.
We’ve seen two ways to load the entity beans, and both require the key. There are no more direct loading methods with the EntityManager—not even to get all entity beans, for example.
Query Options with the EntityManager
Queries via the EntityManager can be made in several ways:
- find(…)/getReference(…): The methods always return only one entity based on the primary key. There are no methods like findAll().
- JPQL: This query language can be thought of as OO SQL.
- Native SQL: JPQL is at the level of perhaps SQL 92 and is therefore more than 30 years old. Databases have evolved massively, and therefore it’s necessary to use Native SQL in some places to take full advantage of the databases’ capabilities.
- Criteria API: This introduces typed queries, and larger queries can be dynamically composed of small building blocks.
To submit JPQL or SQL, the EntityManager declares the following methods:
- createQuery(…) for JPQL queries
- createNativeQuery(…) for Native SQL queries
Two separate methods are needed because if EntityManager only gets a string, it can’t read how to process the queries.
The queries can have placeholders. These are called named queries. These placeholders are written with a colon, just like the NamedParameterJdbcTemplate, so that they are easy to read.
Editor’s note: This post has been adapted from a section of the book Spring Boot 3 and Spring Framework 6 by Christian Ullenboom.
Comments