JPA was born as the acronym for Java Persistence API. When Java EE was donated to the Eclipse Foundation under the Jakarta EE project, the name of the API changed to Jakarta Persistence but the term JPA is still in use. JPA solves the object-relational impedance mismatch by allowing you to map Java objects to database tables and is one of the most (if not the most) used persistence frameworks for Java.
JPA is an API specification that someone can implement–JPA providers or implementations. The most popular JPA implementation is Hibernate ORM. You can use Hibernate ORM without JPA or through JPA. One potential advantage of using it with JPA is that you can move between implementations if you want (something I have never seen happening, though). Another advantage is that someone with experience in, say EclipseLink or Apache OpenJPA, then they can use at least part of that experience when moving to Hibernate.
This article gets you started with the Hibernate implementation of JPA. We’ll use only standard features and cover only the basics. If you are looking for a comprehensive course on JPA or Hibernate, I recommend reading the JPA spec and the official Hibernate docs.
Here’s a video version of this article, in case you want to see the concepts in action:
Here are the Maven dependencies you need to add to the pom.xml file:
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core-jakarta</artifactId>
<version>5.6.4.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>3.0.0</version>
</dependency>
You also need a JDBC driver. For example, if you are using a MariaDB database or SkySQL instance, add the following as well (check the latest version and see what’s new in the 3.0 series):
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.0.3</version>
</dependency>
If you use Gradle, you can use an online Maven to Gradle dependency converter.
You can use any database you want, but let’s continue with MariaDB.
Note: You can create a MariaDB database in the cloud using SkySql. It's free and it doesn’t require credit card details. Create an account and take a look at the documentation for more information on how to get started.
Connect to the database using an SQL client and create the following database:
CREATE DATABASE jpa_demo;
A Persistence Unit is the logical grouping of a set of classes of which objects can be persisted, plus configuration parameters like database connection and pooling options. A Persistence Unit is defined in a persistence.xml file in the src/main/resources/META-INF/ directory. Here’s the one we’ll use in this article:
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="jpa-demo-local" transaction-type="RESOURCE_LOCAL">
<properties>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mariadb://localhost:3306/jpa_demo"/>
<property name="jakarta.persistence.jdbc.user" value="user"/>
<property name="jakarta.persistence.jdbc.password" value="password"/>
<property name="jakarta.persistence.schema-generation.database.action" value="drop-and-create"/>
</properties>
</persistence-unit>
</persistence>
Pay close attention to the name
and transaction-type
parameters. Every Persistence Unit needs a name that we can use later in the code. The transaction type indicates who manages the transactions, JPA or the server. In this example we want JPA to manage the transactions, but in more complex scenarios that require distributed transactions across multiple databases or services like JMS and JCA, we would use JTA instead. This is an advanced topic we won’t cover in this article.
We added properties to specify the JDBC connection URL and the database user and password. We also activated a database action to drop and create the database schema automatically. This will make JPA drop the database (deleting all the tables and schema) and recreate them any time we start the application. This obviously is not a good idea in production environments, but very useful during development, especially when you are starting to define the database objects through JPA Entities.
An Entity is a class of which instances we want to persist in the database. You can configure them through Java annotations or XML. Study the following example paying close attention to the annotations:
package com.example;
import jakarta.persistence.*;
import java.util.Objects;
@Entity
@Table(name = "programming_language")
public class ProgrammingLanguage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "pl_id")
private Integer id;
@Column(name = "pl_name")
private String name;
@Column(name = "pl_rating")
private Integer rating;
public ProgrammingLanguage() {
}
public ProgrammingLanguage(String name, Integer rating) {
this.name = name;
this.rating = rating;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProgrammingLanguage that = (ProgrammingLanguage) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getRating() {
return rating;
}
public void setRating(Integer rating) {
this.rating = rating;
}
}
At a minimum, a JPA Entity class is marked by @Entity
, has a field annotated with @Id
, a default constructor, and field accessors (getters). In the previous example, we added extra configuration. For example, we configured the names of the SQL table and column names, and a generated value to let the database assign primary keys. We also added a custom constructor (additional to the required non-arg constructor), equals(Object)
and hashCode()
methods, and field mutators (setters).
The entry point of the JPA API is the EntityManagerFactory
class. Frameworks like Spring Framework and JakartaEE might provide instances of this type to your application. If you are not using them, you can create an instance specifying the name Persistence Unit to use:
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("jpa-demo-local");
Ideally, you should have only one instance of EntityManagerFactory
for each Persistence Unit per application and create EntityManager
objects (much cheaper to create) as needed. Persistence operations must be inside a transaction boundary:
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
... persistence logic here ...
transaction.commit(); // or transaction.rollback();
The EntityManager
class includes methods to query and update data. For example, if we want to save a ProgrammingLanguage
object in the database, we can add the following persistence logic to the previous snippet of code:
entityManager.persist(new ProgrammingLanguage("Java", 10));
Or if we want to get all the programming languages stored in the database as a list of ProgrammingLanguage
objects:
List<ProgrammingLanguage> list = entityManager.createQuery(
"select p from ProgrammingLanguage p where p.rating > 5",
ProgrammingLanguage.class
).getResultList();
Notice that the query in the string is not SQL. If you recall from where we implemented the Entity (ProgrammingLanguage
) we used the @Table
annotation to configure the name of the table as programming_language
. The query language is called Jakarta Persistence Query Language (JPQL), a platform-independent object-oriented language that is part of the JPA specification. This makes your database queries portable between databases.
Check some of the methods available in the EntityManager
interface to get an idea of all the persistence operations available. This is the interface that you’ll use the most when using JPA directly.
Even though we used the most simple use case and ignored things such as transaction rollback in case of errors or controlled conditions, we need 5 lines of boilerplate code to run a simple query or to save, delete or update an Entity. In a Java SE application like the one we are implementing in this article, it’d be useful to have a utility class to encapsulate all the boilerplate code and make the usage of JPA easier. This utility class should include the logic to create one PersistenceManagerFactory
per application, create a new EntityManager
and begin, close, and commit transactions automatically.
Here’s an example of such a utility class implemented as a singleton that takes care of rolling back a transaction in case of a failure. Take into account that if you are using frameworks like Jakarta EE or Spring Data, you don’t really need this kind of class:
package com.example;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import java.util.function.Function;
public class JPAService {
private static JPAService instance;
private EntityManagerFactory entityManagerFactory;
private JPAService() {
entityManagerFactory = Persistence.createEntityManagerFactory("jpa-demo-local");
}
public static synchronized JPAService getInstance() {
return instance == null ? instance = new JPAService() : instance;
}
public void shutdown() {
if (entityManagerFactory != null) {
entityManagerFactory.close();
instance = null;
}
}
public <T> T runInTransaction(Function<EntityManager, T> function) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
boolean success = false;
try {
T returnValue = function.apply(entityManager);
success = true;
return returnValue;
} finally {
if (success) {
transaction.commit();
} else {
transaction.rollback();
}
}
}
}
By using this class we can simplify the code required to use an EntityManager
. For example, to run a JQL query we only need to enclose the logic in a runInTransaction(Function<EntityManager>, T)
call:
List<ProgrammingLanguage> programmingLanguages = jpaService.runInTransaction(entityManager ->
entityManager.createQuery(
"select p from ProgrammingLanguage p where p.rating > 5",
ProgrammingLanguage.class
).getResultList());
Here’s an example of a full Java application that uses this class to create programming languages with random ratings and show the ones with a rating greater than 5:
package com.example;
import java.util.Arrays;
import java.util.List;
public class Application {
private static final JPAService jpaService = JPAService.getInstance();
public static void main(String[] args) {
try {
createProgrammingLanguages();
printTopProgrammingLanguages();
} finally {
jpaService.shutdown();
}
}
private static void createProgrammingLanguages() {
jpaService.runInTransaction(entityManager -> {
Arrays.stream("Java,C++,C#,JavaScript,Rust,Go,Python,PHP".split(","))
.map(name -> new ProgrammingLanguage(name, (int) (Math.random() * 10)))
.forEach(entityManager::persist);
return null;
});
}
private static void printTopProgrammingLanguages() {
List<ProgrammingLanguage> programmingLanguages = jpaService.runInTransaction(entityManager ->
entityManager.createQuery(
"select p from ProgrammingLanguage p where p.rating > 5",
ProgrammingLanguage.class
).getResultList());
programmingLanguages.stream()
.map(pl -> pl.getName() + ": " + pl.getRating())
.forEach(System.out::println);
}
}
We merely scratched the surface of JPA and Hibernate in this article. Fortunately, being one of the most popular Java specifications, a ton of useful resources are available online.
Also Published Here