As you already know, JPA is a convenient way to work with relational databases in Java/Kotlin applications. JPA is a specification that describes how to make data persistent, defines the interfaces that must be implemented by providers, as well as rules for describing metadata. Providers are specific implementations of the specification, for instance, Hibernate and EclipseLink are two of the most popular providers of the JPA specification. In this topic, you will get acquainted with the main components of JPA, namely as EntityManager, entity state, and PersistenceContext, and learn how they are interconnected.
EntityManager
When using JPA to work with a database, you don't need to worry about SQL queries, but focus on how the state of an entity changes. All the complex work of interacting with the database will be hidden, and you just need to change the state of the entity. EntityManager is responsible for the lifecycle of an entity, and you will use its methods to transfer the entity from one state to another. Once an entity is managed by EntityManager, all of its changes will be automatically propagated to the database.
Now let's learn what states are and at what point in time an entity can be in them.
Entity state
JPA defines only four states between which an entity can transition:
-
New. All entities that were created and never interacted with the entity manager are in the "New" state.
-
Managed. All entities managed by the entity manager or already connected to a row in the database are in the "Managed" state.
-
Detached. If the persistence context closes, all entities it managed will go into the "Detached" state. The same goes for the cases when the "detached" method was called on the entity. All further changes are no longer tracked and automatic saving to the database will not be performed.
-
Removed. All entities that are deleted will go into the "Deleted" status. The actual deletion request from the database will be done later. The process of synchronizing the persistence context with the database is called Flushing.
Without flushing, the changes that have been made will be saved to the persistence context, and can't be accessed by another entity manager. So, when an entity is marked as deleted, it still exists in the database and will only be deleted when the changes are flushed.
The diagram below shows the possible states and the transitions between them.
To change the JPA entity state, you need to use one of the EntityManager methods.
Persistence context
As you already know, EntityManager is responsible for the lifecycle of an entity, and also changes its state. All entities tracked by EntityManager are in their persistence context. The EntityManager tracks change to all entities from the persistence context and flush those changes to the database. As soon as the context is closed, all entities go into the "Detached" state, and from that moment on, the EntityManager no longer tracks changes to these entities and they will not be displayed in the database.
You can create several EntityManagers, but each will be responsible only for certain entities stored in the persistence context. Persistence context is created automatically when the manager is created. You can change the persistence context only through calls to EntityManagers methods.
Practice
Now that you've got acquainted with the main components of JPA, let's now see how they can be used with a simple example. The example will use Hibernate as the JPA implementation and H2 as the database.
To start working with Hibernate and H2, add the following dependencies to your Spring Boot project:
Maven
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
First, let's set up the Spring Boot application class:
Java
package org.hyperskill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
@SpringBootApplication
public class App implements CommandLineRunner {
private final Logger log = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
@Autowired
private LocalContainerEntityManagerFactoryBean entityManagerFactoryBean;
@Override
public void run(String... args) {
// Code will be written here
}
}
Kotlin
package org.hyperskill
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
@SpringBootApplication
class App(
@Autowired
private val entityManagerFactoryBean: LocalContainerEntityManagerFactoryBean
) : CommandLineRunner {
private val log: Logger = LoggerFactory.getLogger(App::class.java)
override fun run(vararg args: String) {
// Code will be written here
}
}
fun main(args: Array<String>) {
runApplication<App>(*args)
}
Imagine that we have an entity called Post with which we will work.
Java
package org.hyperskill;
import javax.persistence.*;
@Entity
@Table(name = "post")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
private int id;
@Column(name = "title")
private String title;
@Column(name = "description")
private String description;
public Post() {
}
public Post(String title, String description) {
this.title = title;
this.description = description;
}
// getters and setters
}
Kotlin
package org.hyperskill;
import javax.persistence.*;
@Entity
@Table(name = "post")
class Post(
@Column(name = "title")
var title: String = "",
@Column(name = "description")
var description: String = ""
) {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
var id = 0
}
Add this line to the application.properties file in order to display sql queries in the console:
spring.jpa.show-sql=true
Let's try to do basic CRUD (Create, Read, Update, Delete) operations.
Java
@Override
public void run(String... args) {
// Step 0: Create post
Post post = new Post("First post title", "Some description for post.");
EntityManager entityManager = entityManagerFactoryBean.getObject().createEntityManager();
// Step 1: Persist
entityManager.persist(post);
log.info("Step 1: The post entity identifier is {}", post.getId());
// Step 2: Select
Post selectedPost = entityManager.find(Post.class, post.getId());
log.info("Step 2: selectedPost: {}", selectedPost.toString());
log.info("Step 2: selectedPost is equal to post: {}", selectedPost == post);
// Step 3: Update
post.setDescription("Step 3: Updated description for first post");
// Step 4: Select
Post selectedPost2 = entityManager.find(Post.class, post.getId());
log.info("Step 4: selectedPost: {}", selectedPost2.toString());
log.info("Step 4: selectedPost is equal to post: {}", selectedPost2 == post);
// Step 5: Select
EntityManager entityManager2 = entityManagerFactoryBean.getObject().createEntityManager();
Post post2 = entityManager2.find(Post.class, post.getId());
log.info("Step 5: post2: {}", post2.toString());
log.info("Step 5: post2 is equal to post: {}", post2 == post);
}
Kotlin
override fun run(vararg args: String) {
// Step 0: Create post
val post = Post("First post title", "Some description for post.")
val entityManager = entityManagerFactoryBean.getObject()!!.createEntityManager()
// Step 1: Persist
entityManager.persist(post)
log.info("Step 1: The post entity identifier is {}", post.id)
// Step 2: Select
val selectedPost = entityManager.find(Post::class.java, post.id)
log.info("Step 2: selectedPost: {}", selectedPost.toString())
log.info("Step 2: selectedPost is equal to post: {}", selectedPost == post)
// Step 3: Update
post.description = "Step 3: Updated description for first post"
// Step 4: Select
val selectedPost2 = entityManager.find(Post::class.java, post.id)
log.info("Step 4: selectedPost: {}", selectedPost2.toString())
log.info("Step 4: selectedPost is equal to post: {}", selectedPost2 == post)
// Step 5: Select
val entityManager2 = entityManagerFactoryBean.getObject()!!.createEntityManager()
val post2 = entityManager2.find(Post::class.java, post.id)
log.info("Step 5: post2: {}", post2.toString())
log.info("Step 5: post2 is equal to post: {}", post2 == post)
}
After running this, you will see the following result in the console (some of the logs have been removed because we are not interested in them):
INFO 72942 --- [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.14.Final
INFO 72942 --- [main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
INFO 72942 --- [main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop table if exists post CASCADE
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table post (id integer not null, description varchar(255), title varchar(255), primary key (id))
INFO 72942 --- [main] org.hyperskill.App : Started App in 8.707 seconds (JVM running for 9.319)
Hibernate: call next value for hibernate_sequence
INFO 72942 --- [main] org.hyperskill.App : Step 1: The post entity identifier is 1
INFO 72942 --- [main] org.hyperskill.App : Step 2: selectedPost: Post{id=1, title='First post title', description='Some description for post.'}
INFO 72942 --- [main] org.hyperskill.App : Step 2: selectedPost is equal to post: true
INFO 72942 --- [main] org.hyperskill.App : Step 4: selectedPost: Post{id=1, title='First post title', description='Step 3: Updated description for first post'}
INFO 72942 --- [main] org.hyperskill.App : Step 4: selectedPost is equal to post: true
Hibernate: select post0_.id as id1_0_0_, post0_.description as descript2_0_0_, post0_.title as title3_0_0_ from post post0_ where post0_.id=?
INFO 72942 --- [main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
ERROR 72942 --- [main] o.s.boot.SpringApplication : Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:771) ~[spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:752) ~[spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:314) ~[spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.7.6.jar:2.7.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.7.6.jar:2.7.6]
at org.hyperskill.App.main(App.java:20) ~[classes/:na]
Caused by: java.lang.NullPointerException: null. // Original error here
at org.hyperskill.App.run(App.java:58) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.7.6.jar:2.7.6]
... 5 common frames omitted
INFO 72942 --- [main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
INFO 72942 --- [main] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
Hibernate: drop table if exists post CASCADE
Hibernate: drop sequence if exists hibernate_sequence
Below is a detailed diagram of everything that happens under the hood. We recommend opening it in a parallel window to make it easier to read the description of each step and see how it's represented in the diagram.
In step 0, we create the Post entity, which is in the New state after creation. We also create an entityManager that we will use to work with the post. To change the post object from the New state to the Managed state, we use the persist method on the entityManager object. After that, in step 2, we use the find method to get a record from the database by id. Next, we check that what we got from the database is the post that we created initially. As you may have noticed, we did not fill in the id field when creating the post object, but after we called the persist entityManager method, we filled this field with the desired value.
In step 3, we set a new value in the description field. At the same time, as you can see, we don't call any methods on the entityManager, since our post object is in the Managed state. This means that we can change it and all changes will be transferred to the persistence context automatically. In step 4, we once again get the post by id and check that all changes have been saved.
In step 5, we create a new entityManger and try to use it to read an object by id from the database.
When you run the code, you will get a NullPointerException when you try to call post2.toString(). We performed all actions from the first to the fourth with the post object without a transaction, and we also did not call the flush method on the entityManager. So, all the changes that were made were saved only in the persistence context of the first entityManager with which we worked. At the same time, nothing was saved to the database, which is why when we used the new entityManager, it couldn't find anything and returned null in step 5.
The diagram below shows what is going on and why we're getting a NullPointerException.
To solve this, we can wrap all our steps from 1 to 4 in a transaction.
Java
@Override
public void run(String... args) {
// Step 0: Create post
Post post = new Post("First post title", "Some description for post.");
EntityManager entityManager = entityManagerFactoryBean.getObject().createEntityManager();
// Create and begin transaction
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
// Step 1: Persist
entityManager.persist(post);
log.info("Step 1: The post entity identifier is {}", post.getId());
// Step 2: Select
Post selectedPost = entityManager.find(Post.class, post.getId());
log.info("Step 2: selectedPost: {}", selectedPost.toString());
log.info("Step 2: selectedPost is equal to post: {}", selectedPost == post);
// Step 3: Update
post.setDescription("Step 3: Updated description for first post");
// Step 4: Select
Post selectedPost2 = entityManager.find(Post.class, post.getId());
log.info("Step 4: selectedPost: {}", selectedPost2.toString());
log.info("Step 4: selectedPost is equal to post: {}", selectedPost2 == post);
// Commit all changes to database
transaction.commit();
// Step 5: Select
EntityManager entityManager2 = entityManagerFactoryBean.getObject().createEntityManager();
Post post2 = entityManager2.find(Post.class, post.getId());
log.info("Step 5: post2: {}", post2.toString());
log.info("Step 5: post2 is equal to post: {}", post2 == post);
}
Kotlin
override fun run(vararg args: String) {
// Step 0: Create post
val post = Post("First post title", "Some description for post.")
val entityManager = entityManagerFactoryBean.getObject()!!.createEntityManager()
// Create and begin transaction
val transaction = entityManager.transaction
transaction.begin()
// Step 1: Persist
entityManager.persist(post)
log.info("Step 1: The post entity identifier is {}", post.id)
// Step 2: Select
val selectedPost = entityManager.find(Post::class.java, post.id)
log.info("Step 2: selectedPost: {}", selectedPost.toString())
log.info("Step 2: selectedPost is equal to post: {}", selectedPost == post)
// Step 3: Update
post.description = "Step 3: Updated description for first post"
// Step 4: Select
val selectedPost2 = entityManager.find(Post::class.java, post.id)
log.info("Step 4: selectedPost: {}", selectedPost2.toString())
log.info("Step 4: selectedPost is equal to post: {}", selectedPost2 == post)
// Commit all changes to database
transaction.commit()
// Step 5: Select
val entityManager2 = entityManagerFactoryBean.getObject()!!.createEntityManager()
val post2 = entityManager2.find(Post::class.java, post.id)
log.info("Step 5: post2: {}", post2.toString())
log.info("Step 5: post2 is equal to post: {}", post2 == post)
}
In this case, when we use commit on a transaction, all changes that have been made are immediately applied to the database. Let's run the new code and make sure that we have SQL queries in the database in the console (some of the logs have been removed because we are not interested in them):
INFO 73986 --- [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.14.Final
INFO 73986 --- [main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
INFO 73986 --- [main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop table if exists post CASCADE
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table post (id integer not null, description varchar(255), title varchar(255), primary key (id))
INFO 73986 --- [main] org.hyperskill.App : Started App in 8.794 seconds (JVM running for 9.431)
Hibernate: call next value for hibernate_sequence
INFO 73986 --- [main] org.hyperskill.App : Step 1: The post entity identifier is 1
INFO 73986 --- [main] org.hyperskill.App : Step 2: selectedPost: Post{id=1, title='First post title', description='Some description for post.'}
INFO 73986 --- [main] org.hyperskill.App : Step 2: selectedPost is equal to post: true
INFO 73986 --- [main] org.hyperskill.App : Step 4: selectedPost: Post{id=1, title='First post title', description='Step 3: Updated description for first post'}
INFO 73986 --- [main] org.hyperskill.App : Step 4: selectedPost is equal to post: true
Hibernate: insert into post (description, title, id) values (?, ?, ?)
Hibernate: update post set description=?, title=? where id=?
Hibernate: select post0_.id as id1_0_0_, post0_.description as descript2_0_0_, post0_.title as title3_0_0_ from post post0_ where post0_.id=?
INFO 73986 --- [main] org.hyperskill.App : Step 5: post2: Post{id=1, title='First post title', description='Step 3: Updated description for first post'}
INFO 73986 --- [main] org.hyperskill.App : Step 5: post2 is equal to post: false
INFO 73986 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
INFO 73986 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
Hibernate: drop table if exists post CASCADE
Hibernate: drop sequence if exists hibernate_sequence
As you can see now, after the entityManager transaction was completed, insert and update requests were made to the database. Now we have the data in the database and, when we look for it using entityManager2, we will find it and will be able to work with it.
As you can see, the post2 object is not equal in reference to the post object, while all the fields are the same. This is because entityManager2 created its object and put it in its persistence context.
Conclusion
JPA is a convenient way to work with relational databases in Java/Kotlin applications. In this topic, you got acquainted with the three main components that you need to be aware of when working with JPA. The first component is the EntityManager which is responsible for the entity's life cycle and which you use to change the entity state. You also learned about the 4 states in which an entity can be and how you can switch between them. Last but not least, we discussed the persistence context. Each EntityManager has its own persistence context, which stores all the entities that the EntityManager must monitor. There are many subtleties when working with JPA, which you will tackle in the future topics.