Appearance
JPA Part 2: Setup, Architecture and the Entity Lifecycle
Part 1 wrote SQL by hand with JdbcTemplate.
ORM (object-relational mapping) is a bridge between Java objects and database tables. With JDBC you work in SQL; with an ORM you work with Java objects, and the framework writes the SQL. In the ORM layer, JPA is the interface and Hibernate (the default in Spring Boot) implements it. You can swap in another implementation such as EclipseLink.
First a working example, then what happens underneath.
A first JPA program
1. The dependency. It brings Hibernate along; you do not add Hibernate yourself.
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>2. The connection properties, as in part 1 (plus the H2 driver dependency):
properties
spring.datasource.url=jdbc:h2:mem:userDB
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# for development only: print the SQL Hibernate runs
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true3. An entity. An entity is a Java class that corresponds to a table. Its fields are the columns, and each object is a row.
java
@Entity
public class UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
public UserDetails() { // required by JPA
}
public UserDetails(String name, String email) {
this.name = name;
this.email = email;
}
// getters and setters
}@Id marks the primary key, and @GeneratedValue(strategy = GenerationType.IDENTITY) lets the database generate it. The class UserDetails becomes the table user_details. JPA needs the no-argument constructor to create objects when it reads rows.
4. A repository for the entity:
java
@Repository
public interface UserDetailsRepository extends JpaRepository<UserDetails, Long> {
}JpaRepository<UserDetails, Long> (entity type, id type) already has save, findById, findAll, deleteById and more. You write no implementation; Spring generates one. You add methods here only when you need your own queries (later parts).
Service and controller as usual:
java
@Service
public class UserDetailsService {
@Autowired
private UserDetailsRepository userDetailsRepository;
public void saveUser(UserDetails user) {
userDetailsRepository.save(user);
}
public List<UserDetails> getAllUsers() {
return userDetailsRepository.findAll();
}
}java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
UserDetailsService userDetailsService;
@GetMapping("/test-jpa")
public List<UserDetails> getUser() {
UserDetails userDetails = new UserDetails("xyz", "xyz@example.com");
userDetailsService.saveUser(userDetails);
return userDetailsService.getAllUsers();
}
}At startup the log shows Hibernate creating the table (for an in-memory database, Spring Boot drops and recreates the tables on each start):
Hibernate: drop table if exists user_details cascade
Hibernate: create table user_details (id bigint generated by default as identity, email varchar(255), name varchar(255), primary key (id))GET /api/test-jpa inserts a row and returns it.
To look inside H2, enable its console and open http://localhost:8080/h2-console (JDBC URL jdbc:h2:mem:userDB, user sa, empty password):
properties
spring.h2.console.enabled=true
spring.h2.console.path=/h2-consoleThe architecture
1. Persistence unit
A persistence unit is a logical group of entity classes that share one configuration: the database connection, the JPA provider (Hibernate, ...), the dialect, and so on. In plain JPA it is written in persistence.xml:
xml
<persistence-unit name="persistentUnitNameHere" transaction-type="RESOURCE_LOCAL">
<!-- the entity classes in this group -->
<class>com.example.entity.SampleEntity1</class>
<class>com.example.entity.SampleEntity2</class>
<!-- which JPA provider: Hibernate, OpenJPA, EclipseLink... -->
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:userDB"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
</properties>
</persistence-unit>In Spring Boot, application.properties plays this role, and most of it is automatic:
- Entities: Spring Boot finds every
@Entityin the main class's package and below. To limit or change where it looks, put@EntityScan("com.example.entity")on a configuration class. - Provider: Hibernate, because it is on the classpath.
- Dialect (the SQL flavour of your database): detected from the connection. Set
spring.jpa.database-platformonly if detection fails. - Transaction type:
RESOURCE_LOCAL.
application.properties assumes one database. An application with two databases defines one persistence unit for each, in configuration classes.
2. EntityManagerFactory
At startup, Spring builds an EntityManagerFactory object from the persistence unit, filling in defaults for anything not given (in LocalContainerEntityManagerFactoryBean). There is one factory per persistence unit: two databases, two factories. As the name says, it creates EntityManager objects.
You can also create it yourself:
java
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setJdbcUrl("jdbc:h2:mem:userDB");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setGenerateDdl(true);
adapter.setDatabasePlatform("org.hibernate.dialect.H2Dialect");
return adapter;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setJpaVendorAdapter(jpaVendorAdapter);
emf.setPackagesToScan("com.example.jpa");
emf.setPersistenceUnitName("uniqueFactoryName"); // unique name for this factory
return emf;
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}3. Transaction manager
After the factory, Spring creates a transaction manager for it, based on the persistence unit's transaction type:
RESOURCE_LOCAL(the default): transactions on one database. Spring Boot creates aJpaTransactionManager(an implementation ofPlatformTransactionManager) tied to the factory, as in the last bean above.JTA(Java Transaction API): transactions that span several databases. That needs extra pieces (XA data sources, a coordinator such as Atomikos, two-phase commit) and is beyond this chapter.
Everything so far happens at startup. The rest happens when an API is called.
4. EntityManager and the persistence context
EntityManager is the JPA interface for working with entities. Opening one is like opening a session with the database (Hibernate calls it Session); closing it ends the session.
| Method | Used for |
|---|---|
persist() | saving a new entity |
merge() | updating (re-attaching) an entity |
find() | fetching by id |
remove() | deleting |
createQuery() | running JPQL queries |
Hibernate implements these methods. The factory creates EntityManager objects.
Each EntityManager has a persistence context: the set of entities it is currently working with. It acts as a first-level cache (part 3) and manages each entity's lifecycle.
Insert, update and delete need a transaction; reads do not. JpaRepository takes care of this: its save, delete and similar methods are already @Transactional:
java
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
entityManager.persist(entity);
return entity;
} else {
return entityManager.merge(entity);
}
}Calling EntityManager yourself without a transaction fails:
java
@Service
public class UserDetailsService {
@PersistenceContext
EntityManager entityManager;
public void saveUser(UserDetails user) {
entityManager.persist(user);
}
}jakarta.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' callAdd @Transactional to saveUser and it works. This is why we normally use JpaRepository: it handles transactions and opening and closing the EntityManager, and adds paging, sorting and batch methods.
Why no SELECT? If the controller saves a user and then fetches it with findById(id) in the same request, the log shows only the INSERT. By default Spring Boot keeps one EntityManager open for the whole web request (spring.jpa.open-in-view=true), so the saved entity was still in its persistence context and the find was answered from there. The same calls outside a web request each get their own EntityManager, and you do see the SELECT.
Entity lifecycle
- New (transient): you created the object with
new. JPA knows nothing about it. - Managed: it is in the persistence context. It gets there in two ways:
persist()a new object, or read an existing row (find()or a query), which loads it into the context. Changes to a managed entity are written to the database when the context is flushed (at the latest at commit). - Removed:
remove()marks it for deletion. The row is deleted at the flush. Until then you can change your mind:persist()makes it managed again. - Detached: no longer managed, because you called
detach()or theEntityManagerwas closed. JPA no longer syncs it; that is now your responsibility.merge()brings its state back under management. Careful:merge()returns a managed copy; the object you passed in stays detached, so keep using the returned one.
Next: The entity lifecycle explains a behavior that surprises many people: reading the same entity twice may run only one SELECT. Part 3 covers that first-level cache.
Check yourself
- What four things do you need for a first JPA program?
- What is an entity? Which table does
UserDetailsmap to? - Why must an entity have a no-argument constructor?
- What is a persistence unit, and what plays its role in Spring Boot?
- How does Spring Boot find entities, and how do you restrict the packages?
- How many
EntityManagerFactoryobjects does an application with two databases have? - What is the difference between
RESOURCE_LOCALandJTA? - Which of these happen at startup: EntityManagerFactory, EntityManager, transaction manager?
- What happens if you call
entityManager.persist()in a method without@Transactional? Why doesrepository.save()not have this problem? - Inside one web request,
save(user)thenfindById(user.getId())prints noSELECT. Why? - Name the four entity states. How does an entity become managed?
- After
UserDetails m = entityManager.merge(detachedUser), which object is managed?
Answers
- The
spring-boot-starter-data-jpadependency, the datasource properties, an@Entityclass, a repository interface extendingJpaRepository. - A Java class corresponding to a table (fields are columns, objects are rows).
user_details. - JPA creates objects through it when it loads rows.
- A group of entities sharing one configuration (connection, provider, dialect, transaction type).
application.propertiesplus auto-configuration. - It scans the main class's package and sub-packages for
@Entity.@EntityScan("package"). - Two, one per persistence unit.
RESOURCE_LOCALhandles transactions on one database;JTAhandles transactions spanning several.- EntityManagerFactory and transaction manager. EntityManagers are created when requests use the database.
TransactionRequiredException.save()inSimpleJpaRepositoryis already@Transactional.- Open EntityManager in View keeps one
EntityManagerfor the request, so the entity was found in its persistence context (first-level cache). - New, managed, removed, detached. By
persist()of a new object, or by being read from the database. m, the returned copy.detachedUserstays detached.