JDBC is verbose
For instance, this is the most common way of inserting some records:
int postCount = 100;
try (PreparedStatement postStatement = connection.prepareStatement("""
INSERT INTO post (
id,
title
)
VALUES (
?,
?
)
"""
)) {
for (int i = 1; i <= postCount; i++) {
int index = 0;
postStatement.setLong(
++index,
i
);
postStatement.setString(
++index,
String.format(
"High-Performance Java Persistence, review no. %1$d",
i
)
);
postStatement.executeUpdate();
}
} catch (SQLException e) {
fail(e.getMessage());
}
JDBC batching requires changing your data access code
And, the moment you realized that this does not perform well, because you forgot to use batching, you have to change the previous implementation, like this:
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());
}
The JPA and Hibernate alternative
With JPA, once you mapped your entity:
@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;
}
}
And, you set the following Hibernate configuration property:
<property name="hibernate.jdbc.batch_size" value="50"/>
This is how you can insert those post
table records:
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
)
)
);
}
Much simpler, right?
Fetching data with JDBC
With JDBC, this is how you execute an SQL projection:
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());
}
That's rather verbose because you have to transform the ResultSet
to the data structure your application is using (e.g., DTOs, JSON web response).
Fetching data with JPA
With JPA, you can fetch the List
of Post
records like this:
int maxResults = 10;
List<Post> posts = entityManager.createQuery("""
select p
from post p
order by p.id
""", Post.class)
.setMaxResults(maxResults)
.getResultList();
And, not only that it's simpler to write it, but it works on every database supported by Hibernate since the pagination syntax is adapted based on the underlying database dialect.
Other advantages JPA has over JDBC
- You can fetch entities or DTOs. You can even fetch hierarchical parent-child DTO projection.
- You can enable JDBC batching without changing the data access code.
- You have support for optimistic locking.
- You have a pessimistic locking abstraction that's independent of the underlying database-specific syntax so that you can acquire a READ and WRITE LOCK or even a SKIP LOCK.
- You have a database-independent pagination API.
hibernate.query.in_clause_parameter_padding
.
- You can use a strongly consistent caching solution that allows you to offload the Primary node, which, for read-write transactions, can only be called vertically.
- You have built-in support for audit logging via Hibernate Envers.
- You have built-in support for multitenancy.
- You can generate an initial schema script from the entity mappings using the Hibernate hbm2ddl tool, which you can supply to an automatic schema migration tool, like Flyway.
- Not only that you have the freedom of executing any native SQL query, but you can use the SqlResultSetMapping to transform the JDBC
ResultSet
to JPA entities or DTOs.
JPA disadvantages
The disadvantages of using JPA and Hibernate are the following:
- While getting started with JPA is very easy, become an expert requires a significant time investment because, besides reading its manual, you still have to learn how database systems work, the SQL standard as well as the specific SQL flavor used by your project relation database.
- There are some less-intuitive behaviors that might surprise beginners, like the flush operation order.
- The Criteria API is rather verbose, so you need to use a tool like Codota to write dynamic queries more easily.
Conclusion
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:
- MyBatis, which is a very lightweight SQL query mapper framework.
- QueryDSL, which allows you to build SQL, JPA, Lucene, and MongoDB queries dynamically.
- jOOQ, which provides a Java metamodel for the underlying tables, stored procedures, and functions and allows you to build an SQL query dynamically using a very intuitive DSL and in a type-safe manner.
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. So, nowadays, you don't really need to use plain JDBC unless you are developing a data access framework.