1. Overview

Spring Data now supports core Java 8 features – such as Optional, Stream API and CompletableFuture.

In this quick article, we’ll go through some examples of how we can use these with the framework.

2. Optional

Let’s start with the CRUD repository methods – which now wrap results in an Optional:

public interface CrudRepository<T, ID> extends Repository<T, ID> {
    
    Optional<T> findById(ID id);
    
}

When returning an Optional instance, it’s a useful hint that there’s a possibility that the value might not exist. More information on Optional can be found here.

All we now have to do is to specify return type as an Optional:

public interface UserRepository extends JpaRepository<User, Integer> {
    
    Optional<User> findOneByName(String name);
    
}

3. Stream API

Spring Data also provides the support for one of the most important features of Java 8 – the Stream API.

In the past, whenever we needed to return more than one result, we needed to return a collection:

public interface UserRepository extends JpaRepository<User, Integer> {
    // ...
    List<User> findAll();
    // ...
}

One of the problems with this implementation was the memory consumption.

We had to eagerly load and keep all retrieved objects in it.

We could improve by leveraging paging:

public interface UserRepository extends JpaRepository<User, Integer> {
    // ...
    Page<User> findAll(Pageable pageable);
    // ...
}

In some scenarios, that’s enough, but in others – pagination is really not the way to go, due to the high number of requests necessary to retrieve the data.

Thanks to Java 8 Stream API and JPA providers – we can now define that our repository method returns just a Stream of objects:

public interface UserRepository extends JpaRepository<User, Integer> {
    // ...
    Stream<User> findAllByName(String name);
    // ...
}

Spring Data uses provider-specific implementation to stream the result (Hibernate uses ScrollableResultSet, EclipseLink uses ScrollableCursor). It reduces the amount of memory consumption and query calls to a database. Because of that, it’s also much faster than two solutions mentioned earlier.

Processing data with a Stream requires us to close a Stream when we finish it.

It can be done by calling the close() method on a Stream or by using try-with-resources:

try (Stream<User> foundUsersStream 
  = userRepository.findAllByName(USER_NAME_ADAM)) {
 
assertThat(foundUsersStream.count(), equalTo(3l));

We must also remember to call a repository method within a transaction. Otherwise, we’ll get an exception:

org.springframework.dao.InvalidDataAccessApiUsageException: You’re trying to execute a streaming query method without a surrounding transaction that keeps the connection open so that the Stream can actually be consumed. Make sure the code consuming the stream uses @Transactional or any other way of declaring a (read-only) transaction.

4. CompletableFuture

Spring Data repositories can run asynchronously with the support of Java 8’s CompletableFuture and Spring mechanism for asynchronous method execution:

@Async
CompletableFuture<User> findOneByStatus(Integer status);

A client which calls this method will return a future immediately but a method will continue an execution in a different thread.

More info about CompletableFuture processing can be found here.

5. Conclusion

In this tutorial, we showed how Java 8 features work together with Spring Data.

The full implementation of the examples is available over on Github.