
Transactions and Concurrency Control
Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?
What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?
Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.
So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!
If you are wondering why and when you should use JPA or Hibernate, then this article is going to provide you an answer to this very common question. Because I’ve seen this question asked very often on the /r/java Reddit channel, I decided that it’s worth writing an in-depth answer about the strengths and weaknesses of JPA and Hibernate.
Although JPA has been a standard since it was first released in 2006, it’s not the only way you can implement a data access layer using Java. We are going to discuss the advantages and disadvantages of using JPA or any other popular alternatives.
In 1997, Java 1.1 introduced the JDBC (Java Database Connectivity) API, which was very revolutionary for its time since it was offering the possibility of writing the data access layer once using a set of interfaces and run it on any relational database that implements the JDBC API without needing to change your application code.
The JDBC API offered a Connection interface to control the transaction boundaries and create simple SQL statements via the Statement API or prepared statements that allow you to bind parameter values via the PreparedStatement API.
So, assuming we have a post database table and we want to insert 100 rows, here’s how we could achieve this goal with JDBC:
int postCount = 100;
int batchSize = 50;
try (PreparedStatement postStatement = connection.prepareStatement("""
INSERT INTO post (
id,
title
)
VALUES (
?,
?
)
"""
)) {
for (int i = 1; i <= postCount; i++) {
if (i % batchSize == 0) {
postStatement.executeBatch();
}
int index = 0;
postStatement.setLong(
++index,
i
);
postStatement.setString(
++index,
String.format(
"High-Performance Java Persistence, review no. %1$d",
i
)
);
postStatement.addBatch();
}
postStatement.executeBatch();
} catch (SQLException e) {
fail(e.getMessage());
}
While we took advantage of multi-line Text Blocks and try-with-resources blocks to eliminate the PreparedStatement close call, the implementation is still very verbose. Note that the bind parameters start from 1, not 0 as you might be used to from other well-known APIs.
To fetch the first 10 rows, we could need to run an SQL query via the PreparedStatement, which will return a ResultSet representing the table-based query result. However, since applications use hierarchical structures, like JSON or DTOs to represent parent-child associations, most applications needed to transform the JDBC ResultSet to a different format in the data access layer, as illustrated by the following example:
int maxResults = 10;
List<Post> posts = new ArrayList<>();
try (PreparedStatement preparedStatement = connection.prepareStatement("""
SELECT
p.id AS id,
p.title AS title
FROM post p
ORDER BY p.id
LIMIT ?
"""
)) {
preparedStatement.setInt(1, maxResults);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
while (resultSet.next()) {
int index = 0;
posts.add(
new Post()
.setId(resultSet.getLong(++index))
.setTitle(resultSet.getString(++index))
);
}
}
} catch (SQLException e) {
fail(e.getMessage());
}
Again, this is the nicest way we could write this with JDBC as we’re using Text Blocks, try-with-resources, and a Fluent-style API to build the Post objects.
Nevertheless, the JDBC API is still very verbose and, more importantly, lacks many features that are required when implementing a modern data access layer, like:
ReusltSet and extract the column values to set the Post object properties.In 1999, Sun released J2EE (Java Enterprise Edition), which offered an alternative to JDBC, called Entity Beans.
However, since Entity Beans were notoriously slow, overcomplicated, and cumbersome to use, in 2001, Gavin King decided to create an ORM framework that could map database tables to POJOs (Plain Old Java Objects), and that’s how Hibernate was born.
Being more lightweight than Entity Beans and less verbose than JDBC, Hibernate grew more and more popular, and it soon became the most popular Java persistence framework, winning over JDO, iBatis, Oracle TopLink, and Apache Cayenne.
Learning from the Hibernate project success, the Java EE platform decided to standardize the way Hibernate and Oracle TopLink, and that’s how JPA (Java Persistence API) was born.
JPA is only a specification and cannot be used on its own, providing only a set of interfaces that define the standard persistence API, which is implemented by a JPA provider, like Hibernate, EclipseLink, or OpenJPA.
When using JPA, you need to define the mapping between a database table and its associated Java entity object:
@Entity
@Table(name = "post")
public class Post {
@Id
private Long id;
private String title;
public Long getId() {
return id;
}
public Post setId(Long id) {
this.id = id;
return this;
}
public String getTitle() {
return title;
}
public Post setTitle(String title) {
this.title = title;
return this;
}
}
Afterward, we can rewrite the previous example which saved 100 post records looks like this:
for (long i = 1; i <= postCount; i++) {
entityManager.persist(
new Post()
.setId(i)
.setTitle(
String.format(
"High-Performance Java Persistence, review no. %1$d",
i
)
)
);
}
To enable JDBC batch inserts, we just have to provide a single configuration property:
<property name="hibernate.jdbc.batch_size" value="50"/>
Once this property is provided, Hibernate can automatically switch from non-batching to batching without needing any data access code change.
And, to fetch the first 10 post rows, we can execute the following JPQL query:
int maxResults = 10;
List<Post> posts = entityManager.createQuery("""
select p
from post p
order by p.id
""", Post.class)
.setMaxResults(maxResults)
.getResultList();
If you compare this to the JDBC version, you will see that JPA is much easier to use.
JPA, in general, and Hibernate, in particular, offer many advantages.
List of values to an IN query clause, as explained in this article.ResultSet to JPA entities or DTOs.The disadvantages of using JPA and Hibernate are the following:
JPA and Hibernate are extremely popular. According to the 2018 Java ecosystem report by Snyk, Hibernate is used by 54% of every Java developer that interacts with a relational database.
This result can be backed by Google Trends. For instance, if we compare the Google Trends of JPA over its main competitors (e.g., MyBatis, QueryDSL, and jOOQ), we can see that JPA is many times more popular and shows no signs of losing its dominant market share position.

Being so popular brings many benefits, like:
One of the greatest things about the Java ecosystem is the abundance of high-quality frameworks. If JPA and Hibernate are not a good fit for your use case, you can use any of the following frameworks:
So, use whatever works best for you.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
In this article, we saw why JPA was created and when you should use it. While JPA brings many advantages, you have many other high-quality alternatives to use if JPA and Hibernate don’t work best for your current application requirements.
And, sometimes, as I explained in this free sample of my High-Performance Java Persistence book, you don’t even have to choose between JPA or other frameworks. You can easily combine JPA with a framework like jOOQ to get the best of both worlds.
