JPAstreamer – JPA Entities as Java Stream in Spring Boot

JPAstreamer is a library for expressing JPA/Hibernate/Spring queries using standard Java streams. It helps in fetching and processing the database records in the same way as we process the POJOs.

For example, we can fetch all inactive users from the database using the following code:

List<User> inactiveUsers = jpaStreamer.stream(User.class)
        .filter(u -> !u.getActive())
        .collect(Collectors.toList());

Internally, JPAstreamer will generate an optimized SQL query that will run in the database using the datasource/entity manager configured in the application.

For the demo, we will use the following User entity class:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;
  private String name;
  private LocalDate dateOfBirth;
  private Boolean active;
}

1. Getting Started with JPAstreamer

In a typical Spring boot application, we can import and configure JPAstreamer by importing the jpastreamer-core and spring-boot-jpastreamer-autoconfigure dependencies.

The jpastreamer-core dependency transitively imports all the necessary dependencies and sub-modules. The spring-boot-jpastreamer-autoconfigure helps in auto-configuring the JPAstreamer in Spring boot applications using its autoconfiguration when we already have spring-boot-starter-data-jpa in the classpath.

<dependency>
  <groupId>com.speedment.jpastreamer</groupId>
  <artifactId>jpastreamer-core</artifactId>
  <version>1.1.4</version>
</dependency>

<dependency>
  <groupId>com.speedment.jpastreamer.integration.spring</groupId>
  <artifactId>spring-boot-jpastreamer-autoconfigure</artifactId>
  <version>1.1.4</version>
</dependency>

Note that currently, JPAstreamer works with Spring boot 2. Due to incompatibility with ‘jakarta.*‘ namespace, it will not work with Spring boot 3. You may check the issue tracker for the latest updates.

2. Initializing JPAStreamer

In a Spring boot application, the autoconfiguration automatically initializes the JPAStreamer class which provides a fully type-safe API for writing JPA queries. We can autowire it in a service class or test class as follows:

@Service
public class UserService {

  @Autowired
  JPAStreamer jpaStreamer;

  ...
}

We can initialize the JPAstreamer with a persistence unit name as well. In the following code, “persistence-unit-name” is to be replaced with the name of your persistence unit.

JPAStreamer jpaStreamer = JPAStreamer.of("persistence-unit-name");

Behind the scene, JPAStreamer automatically generates a metamodel by an annotation processor that operates at compile time as soon as JPAstreamer has been installed. The annotation processor inspects all classes annotated with @Entity. For example, ‘User.class‘ generates an equivalent ‘User$.class’ in the project’s '/target' folder.

Next, a stream renderer is used in runtime that translates the stream operations into JPA queries for performance reasons.

3. Getting Stream from a Table

To stream entities from a table, we must use the stream(entity-class). Once we obtain the stream of records, we can process the records similar to standard Java streams.

jpaStreamer.stream(User.class)
	//other stream operations...

Do not forget to close the stream to release any resources potentially held by JPAstreamer.

jpaStreamer.close();

4. Working with Entity Streams

Once we have the stream of entities, we can perform all kinds of intermediate and terminal operations as we do in standard Java streams. Read the official page for the complete list of supported features.

4.1. Filtering with Predicates

In the generated metadata class, each field is created as ComparableField and we can apply the inbuilt Comparators to filter the stream elements.

For example, to find all users in the database that have dateOfBirth field as null, we can use the following statement:

List<User> usersWithoutDateOfBirth = jpaStreamer.stream(User.class)
        .filter(User$.dateOfBirth.isNull())
        .collect(Collectors.toList());

The following table lists a few predicates for our understanding. You can read about all supported predicates in the official guide. Note that we can create a composed predicate by combining multiple predicates using and() and or() methods.

PredicateDescription
isNull, isNotNullthe field is null or not null.
equal, notEqualthe field is equal / not equal to the parameter.
lessOrEqual, greaterOrEqual, lessThan, greaterThanthe field is lesser / greater / equal than the parameter.
between, notBetweenthe field is between / not between the start (inclusive) and end (exclusive).
in, notInthe array parameter contains / not contains the field.
isEmpty, isNotEmptythe string is empty / not empty.
startsWith, notStartsWith, startsWithIgnoreCase, notStartsWithIgnoreCasethe string starts with / not starts with the given parameter in case-insensitive /case-sensitive manner.
endsWith, notEndsWith, endsWithIgnoreCase, notEndsWithIgnoreCasethe string ends with / not ends with the given parameter in case-insensitive /case-sensitive manner.
contains, notContains, containsIgnoreCase, notContainsIgnoreCasethe string contains / not contains the given parameter in case-insensitive /case-sensitive manner.
negatenegates a given predicate.
and, orreturns a composed predicate that represents a short-circuiting logical AND / OR of two given predicates.

Note that the list of supported predicates depends on the field’s datatype. For example, isNull() and isNotNull() are applicable only for non-primitive type fields.

4.2. Sorting

We can use the sorted() of Stream interface and pass the necessary comparator to sort the entities. For reverse sorting, use the reverse() comparator.

jpaStreamer.stream(User.class)
	.sorted(User$.dateOfBirth.reversed()) 
	...

4.3. Pagination

The library also supports the pagination feature similar to plain JPA criteria queries. The page numbers start with 0. In the following statement, we are fetching page number 2 while the page size is 10.

int page = 2;
int PAGE_SIZE = 10;

List<User> users = jpaStreamer.stream(User.class)
    skip(page * PAGE_SIZE)
    .limit(PAGE_SIZE)
    .toList();

5. Transactions

While working with entities in the stream, we can also update the entities using the EntityManager reference. It is necessary to ensure these updates happen inside a transaction boundary. We can begin and end the transactions using the simple JPA methods as follows:

@Autowired
EntityManagerFactory emf;

public void updateUsers() {

	EntityManager em = emf.createEntityManager();
	JPAStreamer jpaStreamer = JPAStreamer.of(emf); 

  try {
    em.getTransaction().begin();

    jpaStreamer.stream(User.class) 
      .filter(User$.active.equal(false)) 
      .forEach(u -> {
        u.setActive(true)); 
        em.merge(u); 
      }

    em.getTransaction().commit();
  } catch(Exception e) {

    em.getTransaction().rollback();
  } 
}

6. Conclusion

In this JPAstreamer tutorial, we learned the fundamentals of the library. We learned to import and setup JPAstreamer in a Spring boot application that already has spring-boot-starter-data-jpa dependency. We also learned to create and autowire the JPAstreamer instance and stream data from a table using the various stream operations.

Happy Learning !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode