CRUD operations
Once you’ve mapped a class to a database table and established its primary key, you have everything you need to create, retrieve, delete, and update that class in the database. Calling entityManager.save()
will create or update the specified class, depending on whether the primary-key field is null or applies to an existing entity. Calling entityManager.remove()
will delete the specified class.
Entity relationships
Simply persisting an object with a primitive field is only half the equation. JPA also lets you manage entities in relation to one another. Four kinds of entity relationships are possible in both tables and objects:
- One-to-many
- Many-to-one
- Many-to-many
- One-to-one
Each type of relationship describes how an entity relates to other entities. For example, the Musician
entity could have a one-to-many relationship with Performance
, an entity represented by a collection such as List
or Set
.
If the Musician
included a Band
field, the relationship between these entities could be many-to-one, implying a collection of Musician
s on the single Band
class. (Assuming each musician only performs in a single band.)
If Musician
included a BandMates
field, that could represent a many-to-many relationship with other Musician
entities. (In this case the musician rows/objects are self-referencing, another common pattern.)
Finally, Musician
might have a one-to-one relationship with a Quote
entity, used to represent a famous quote: Quote famousQuote = new Quote()
.
Defining relationship types
JPA has annotations for each of its relationship mapping types. The following code shows how you might annotate the one-to-many relationship between Musician
and Performances
. In this case, each musician might have many performances, but there is only one musician for each performance:
// Performance.java
@Entity
public class Performance {
@Id
@GeneratedValue
private Long id;
private String title; // e.g., "Live at Abbey Road"
// Many Performances belong to one Musician
// @JoinColumn specifies the foreign key column in the 'Performance' table
@ManyToOne
@JoinColumn(name = "musician_id") // This will be the FK column in the 'performance' table
private Musician musician;
// constructor and members...
}
public class Musician {
@OneToMany(mappedBy = "musician")
private List performances = new ArrayList();
//...
}
Notice that the @JoinColumn
tells JPA what column on the Performance
table will map to the Musician
entity. Each Performance
will be associated with a single Musician
, which is tracked by this column. When JPA loads a Musician
or Performance
object into the database, it will use this information to reconstitute the object graph.
For Musician
, the @OneToMany(mappedBy = 'musician')
annotation tells JPA to use the Performance.musician
field to populate the performances List
on the Musician
object. (That is, the Performance.musician
field points from the Performance
table to the Musician
table.)
When JPA loads the foreign key from Performance
, it will populate the actual Musician
object found at that primary key in the Musician
table, and the live List
of performances hydrated by the performances holding those foreign keys. As a result, the performances are loaded holding a reference to the Musician
objects, and these objects are loaded holding List
s of the performances.
There is more we can do to fine-tune how these relationships work. Right now, we’re just touching on the basics.
Also see: Java persistence with JPA and Hibernate: Entities and relationships.
JPA fetching strategies
In addition to knowing where to place related entities in the database, JPA needs to know how you want them loaded. Fetching strategies tell JPA how to load related entities. When loading and saving objects, a JPA framework must provide the ability to finetune how object graphs are handled. For instance, if the Musician
class has a bandMate
field, loading GeorgeHarrison
could cause the entire Musician
table to be loaded from the database!
You can use annotations to customize your fetching strategies, but JPA’s default configuration often works out of the box:
- One-to-many: Lazy
- Many-to-one: Eager
- Many-to-many: Lazy
- One-to-one: Eager
Transactions in JPA
While outside the scope of this introduction, transactions allow the developer to define boundaries for groups of operations to the database. We can define several operations together and then execute them together with entityManager.getTransaction().commit()
. If any of the related operations fails, the whole transaction will rollback. This is another essential component of data design.
Transactions can be defined in a variety of ways, from explicit interactions via the API, to using annotations to define transactional boundaries, to using Spring AOP to define the boundaries.
JPA installation and setup
We’ll conclude with a quick look at installing and setting up JPA for your Java applications. For this demonstration we will use EclipseLink, the JPA reference implementation.
The common way to install JPA is to include a JPA provider into your project:
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>4.0.7</version>
</dependency>
We also need to include a database driver:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
Then, we need to tell the system about our database and provider, which we do in a persistence.xml
file:
http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="MyUnit" transaction-type="RESOURCE_LOCAL">
<properties>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/foo_bar"/>
<property name="jakarta.persistence.jdbc.user" value=""/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
</properties>
</persistence-unit>
</persistence>
There are other ways to provide this information to the system, including programmatically. I recommend using the persistence.xml
file because storing dependencies this way makes it very easy to update your application without modifying any code.
Spring configuration for JPA
Using Spring Data JPA will greatly ease the integration of JPA into your application. As an example, placing the @SpringBootApplication
annotation in your application header instructs Spring to automatically scan for classes and inject the EntityManager
as required, based on the configuration you’ve specified.
Include the following dependencies in your build if you want Spring’s JPA support for your application:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.5.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.5.3</version>
</dependency>
When to use JPA
The question of whether to use JPA is a common source of analysis paralysis when designing a Java application. Especially when attempting to make up-front technology decisions, you don’t want to get data persistence—an essential and long-term factor—wrong.
To break this kind of paralysis, it’s useful to remember that applications can evolve into using JPA. You might build exploratory or prototype code using JDBC, then start adding in JPA. There’s no reason these solutions can’t coexist.
After being paralyzed by indecision, the next worst thing is to adopt JPA when the additional effort it implies will prevent a project from moving forward. JPA can be a win for overall system stability and maintainability, but sometimes simpler is better, especially at the beginning of a project. If your team doesn’t have the capacity to adopt JPA up front, consider putting it on your roadmap for the future.
Conclusion
Every application that interfaces with a database should define an application layer whose sole purpose is to isolate persistence code. As you’ve seen in this article, the Jakarta Persistence API (JPA) introduces a range of capabilities and support for Java object persistence. Simple applications may not require every JPA capability, and in some cases the overhead of configuring the framework may not be merited. As an application grows, however, JPA really earns its keep. Using JPA keeps your object code simple and provides a conventional framework for accessing data in Java applications.