How JPAstreamer Can Help You Write Type-Safe Hibernate Code Without Unnecessary Complexity

by Mislav Miličević

on October 8, 2020

For 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 NAMECOLUMN TYPE
person_idint
first_namevarchar(45)
last_namevarchar(45)
created_attimestamp

To represent said table via JPA, users would typically create a class called Person which looks like this:


@Entity
@Table(name = "person")
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "person_id", nullable = false, updatable = false)
    private Integer actorId;

    @Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)")
    private String firstName;

    @Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)")
    private String lastName;

    @Column(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:


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:


final EntityManager entityManager = ...;

final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Person.class);

final Root 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:


<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:


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:


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:


final Stream 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:


final Optional 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:


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:


final EntityManager entityManager = ...;

final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Person.class);

final Root 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 query = entityManager.createQuery(criteriaQuery);
query.setFirstResult(5);
query.setMaxResult(10);

final List 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 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:


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 two 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:


<?xml version="1.0" encoding="UTF-8"?>
<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

About

Mislav Miličević

Mislav Miličević started to develop at the age of 11. When he was 14 years old he began making custom client and server modifications for the game Minecraft. Today he is an experienced Java developer and blogger working as a software developer at Speedment.