How JPAstreamer Can Help You Write Type-Safe Hibernate Code Without Unnecessary Complexity
Learn how to use Hibernate in a type-safe manner without unnecessary complexity using standard Java Streams.
Join the DZone community and get the full member experience.
Join For FreeFor the past 10 or so years, JPA has been one of the most popular ways of accessing a database within the Java ecosystem. Even if you haven't heard of JPA directly, there is a high likelihood that you've heard of or even used one of the most popular implementations of the JPA API - Hibernate.
The reason why JPA is so popular is no secret - most of the inconveniences are abstracted from the user, making the API very intuitive to use. Let's say that in some random database exists a table called person with the following structure:
COLUMN NAME COLUMN TYPE
person_id int
first_name varchar(45)
last_name varchar(45)
created_at timestamp
To represent said table via JPA, users would typically create a class called Person which looks like this:
xxxxxxxxxx
name = "person") (
public class Person {
strategy = GenerationType.IDENTITY) (
name = "person_id", nullable = false, updatable = false) (
private Integer actorId;
name = "first_name", nullable = false, columnDefinition = "varchar(45)") (
private String firstName;
name = "last_name", nullable = false, columnDefinition = "varchar(45)") (
private String lastName;
name = "created_at", nullable = false, updatable = false) (
private LocalDateTime createdAt;
public Integer getActorId() {
return actorId;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}
Once this initial setup is finished, users can use the EntityManager to create and execute queries. Let's say we wanted to retrieve information about a person with the id of 10. With JPA we could do something like this:
xxxxxxxxxx
final EntityManager em = ...;
final Person person = em.createQuery("SELECT Person person FROM person WHERE person.id = 10", Person.class).getSingleResult();
I consider this to be a very efficient approach considering most of the heavy lifting is done for us. But not everything is sunshine and rainbows. Since we're creating queries using raw SQL, syntax errors are inevitable most of the time.
To combat this, a type-safe way of creating queries was introduced in JPA 2.0, named the Criteria API. Let's see how we can recreate the query above using the Criteria API:
xxxxxxxxxx
final EntityManager entityManager = ...;
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Person> criteriaQuery = criteriaBuilder.createQuery(Person.class);
final Root<Person> root = criteriaQuery.from(Person.class);
criteriaQuery.select(root);
final Predicate condition = criteriaBuilder.equal(root.get("personId"), 1);
criteriaQuery.where(condition);
final Person person = entityManager.createQuery(criteriaQuery).getSingleResult();
The difference between the 2 approaches is quite evident. While we're able to create and execute queries in an almost type-safe way (e.g. root.get("personId") is not safe), our code has gotten substantially more complex. Ideally, we want to construct type-safe queries in a manner that doesn't introduce unnecessary complexity in our codebase.
In this article, I'll be showing you how to achieve this goal using JPAstreamer, a lightweight JPA extension that allows us to construct queries in a type-safe way using Java Streams.
Installing JPAstreamer
Before we can start using JPAstreamer we must install it as a dependency in our project. If you're using Maven as your build tool, add the following dependency:
xxxxxxxxxx
<dependencies>
...
<dependency>
<groupId>com.speedment.jpastreamer</groupId>
<artifactId>core</artifactId>
<version>${jpastreamer-version}</version>
</dependency>
...
</dependencies>
If you're using Gradle as your build tool, add the following dependency and annotation processor:
xxxxxxxxxx
dependencies {
compile "com.speedment.jpastreamer:core:${jpastreamer-version}"
annotationProcessor "com.speedment.jpastreamer:fieldgenerator-standard:${jpastreamer-version}"
}
JPAstreamer Setup
To get access to a JPAstreamer instance, use the following bit of code:
xxxxxxxxxx
final JPAStreamer jpaStreamer = JPAStreamer.of("my-persistence-unit");
Make sure you're using the correct persistence unit name when creating a JPAStreamer instance. I've called mine my-persistence-unit in the template above, so that's the name I will be passing to JPAStreamer::of.
Creating a Stream
The JPAStreamer instance has a stream() method which you can use to create a Stream for your entities. As an example, I'll use the Person entity created above as a Stream source:
xxxxxxxxxx
final Stream<Person> personStream = jpaStreamer.stream(Person.class);
Once you've obtained a Stream for our Entity, you can start writing queries using the Java Stream API.
Executing a Query
As a start, let's try to recreate the JPA query we created at the beginning of the article, but this time we'll use the Stream API:
xxxxxxxxxx
final Optional<Person> result = personStream
.filter(Person$.personId.equal(1))
.findAny();
By terminating this Stream a series of optimizations and translations take place which in the end create and execute the following SQL Query:
xxxxxxxxxx
select
person0_.person_id as person_id1_0_,
person0_.first_name as first_na2_0_,
person0_.last_update as last_nam3_0_,
person0_.created_at as created_4_0_,
from
person person0_
where
person0_.person_id=1
A More Complex Example
The example that we've been working with so far is rather simple, so let's try creating a JPA query that is a bit more complex in nature:
xxxxxxxxxx
final EntityManager entityManager = ...;
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Person> criteriaQuery = criteriaBuilder.createQuery(Person.class);
final Root<Person> root = criteriaQuery.from(Person.class);
criteriaQuery.select(root);
final Predicate condition = criteriaBuilder.and(
criteriaBuilder.like(root.get("firstName"), "C%"),
criteriaBuilder.like(root.get("lastName"), "M%")
);
final Order primaryOrder = criteriaBuilder.asc(root.get("lastName"));
final Order secondaryOrder = criteriaBuilder.asc(root.get("firstName"));
criteriaQuery.where(condition);
criteriaQuery.orderBy(primaryOrder, secondaryOrder);
final TypedQuery<Person> query = entityManager.createQuery(criteriaQuery);
query.setFirstResult(5);
query.setMaxResult(10);
final List<Person> result = query.getResultList();
A lot of things are happening here, so let's break them down. The query will look for Person entities whose first name starts with the letter C and the last name starts with the letter M. Alongside these conditions, the results will be sorted by the last name and first name. Additionally, an offset of 5 is applied to the query, meaning the first 5 elements of the result will be skipped. Also, the result is limited to 10 entities.
Now let's see how this can be replicated using the Stream API:
xxxxxxxxxx
jpaStreamer.stream(Person.class)
.filter(Person$.firstName.startsWith("C").and(Person$.lastName.startsWith("lastName")))
.sorted(Person$.lastName.comparator().thenComparing(Person$.firstName.comparator()))
.skip(5)
.limit(10)
.collect(Collectors.toList())
The 2 approaches produce identical results, but the declarative style of programming that Java Streams provide us with makes our code less complex, allowing us to develop software a lot more efficiently.
Optional: Setting up a Persistence Unit
While this is not a JPA tutorial, it is important to mention that you must have a persistence unit ready. If you're adding JPAstreamer to an existing JPA project, then you probably have a persistence unit already created.
For those of you who are starting from scratch or have never configured JPA before, you must create a file named persistence.xml in your resources/META-INF folder. I'll provide you with a template for the persistence.xml file which you can modify:
xxxxxxxxxx
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="my-persistence-unit" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<properties>
<property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/db" />
<property name="javax.persistence.jdbc.user" value="root" />
<property name="javax.persistence.jdbc.password" value="password" />
</properties>
</persistence-unit>
</persistence>
Summary
JPA is a great API when it comes to database access, but its usage can easily lead to unnecessary complexity. With JPAstreamer, you're able to utilize JPA while keeping your codebase clean and maintainable.
GitHub: github.com/speedment/jpa-streamer
Homepage: jpastreamer.org
Documentation: speedment.github.io/jpa-streamer
Gitter Support Chat: gitter.im/jpa-streamer
Published at DZone with permission of Mislav Milicevic. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments