Spring Data repository: list vs stream

Viewed 11139

What are recommendations when to define method list and stream in Spring Data repository?

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-streaming

Example:

interface UserRepository extends Repository<User, Long> {

  List<User> findAllByLastName(String lastName);

  Stream<User> streamAllByFirstName(String firstName);                    
         
  // Other methods defined.
}

Please, note, here I am not asking about Page, Slice - they are clear to me, and I found their description in the documentation.


My assumption (am I wrong?):

  1. Stream does not load all the records into Java Heap. Instead it loads k records into the heap and processes them one by one; then it loads another k records and so on.

  2. List does load all the records into Java Heap at once.

  3. If I need some background batch job (for example calculate analytics), I could use stream operation because I will not load all the records into the heap at once.

  4. If I need to return a REST response with all the records, I will need to load them into RAM anyway and serialize them into JSON. In this case, it makes sense to load a list at once.


I saw that some developers collect the stream into a list before returning a response.

class UserController {

    public ResponseEntity<List<User>> getUsers() {
        return new ResponseEntity(
                repository.streamByFirstName()
                        // OK, for mapper - it is nice syntactic sugar. 
                        // Let's imagine there is not map for now...
                        // .map(someMapper)  
                       .collect(Collectors.toList()), 
                HttpStatus.OK);
    }
}

For this case, I do not see any advantage of Stream, using list will make the same end result.

Are then any examples when using list is justified?

2 Answers

tl;dr

The primary difference in Collection VS Stream are the following two aspects:

  1. Time to first result – when does the client code see the first element?
  2. The state of resources while processing - in what state are underlying infrastructure resources while the stream is processed?

Working with collections

Let's talk this through with an example. Let's say we need to read 100k Customer instances from a repository. The way you (have to) handle the result gives a hint at both of the aspects described above.

List<Customer> result = repository.findAllBy();

The client code will receive that list once all elements have been completely read from the underlying data store, not any moment before that. But also, underlying database connections can have been closed. I.e. e.g. in a Spring Data JPA application you will see the underlying EntityManager be closed and the entity detached unless you actively keep that in a broader scope, e.g. by annotating surrounding methods with @Transactional or using an OpenEntityManagerInViewFilter. Also, you don't need to actively close the resources.

Working with streams

A stream will have to be handled like this:

@Transactional
void someMethod() {

  try (Stream result = repository.streamAllBy()) {
    // … processing goes here
  }
}

With a Stream, the processing can start as soon as the first element (e.g. row in a database) arrives and is mapped. I.e. you will be able to already consume elements while others of the result set are still processed. That also means, that the underlying resources need to actively be kept open and as they're usually bound to the repository method invocation. Note how the Stream also has to actively be closed (try-with-resources) as it binds underlying resources and we somehow have to signal it to close them.

With JPA, without @Transactional the Stream will not be able to be processed properly as the underlying EntityManager is closed on method return. You'd see a few elements processed but an exception in the middle of the processing.

Downstream usage

So while you theoretically can use a Stream to e.g. build up JSON arrays efficiently, it significantly complicates the picture as you need to keep the core resources open until you've written all elements. That usually means writing the code to map objects to JSON and writing them to the response manually (using e.g. Jackson's ObjectMapper and the HttpServletResponse.

Memory footprint

While the memory footprint will likely improve, this mostly stems from the fact that you're like avoiding the intermediate creation of collections and additional collections in mapping steps (ResultSet -> Customer -> CustomerDTO -> JSON Object). Elements already processed are not guaranteed to be evicted from memory as they might be held onto for other reasons. Again, e.g. in JPA you'd have to keep the EntityManager open as it controls the resource lifecycle and thus all elements will stay bound to that EntityManager and will be kept around until all elements are processed.

Stream and Collection both have Collection of Objects, but the problem with Collection and its implementations is that Collection implementation have all the elements in memory, actually Stream is introduced in Java8 to tackle this problem(and some other problems). Imagine what happened if you have Collection with infinite number of elements, Could you have Collection with infinite number of elements? sure you can't because no matter how large your memory is, you will get out off memory exception at some point. but Stream does not have this problem you can have infinite number of elements with Stream because they are not stored in memory, they will be generated on demand.

Back to your question imagine what happen if you have many many records that have the lastname in your first query findAllByLastName? Sure you will get OutOfMemoryError exception but Stream solve this problem no matter how many record meet your criteria you won't get OutOfMemoryError exception. Stream does not load object in memory it loads objects on demand, So it performs better on large result queries.

So the answer to your questions:

  1. Yes, it loads elements to memory on demand so it reduces the amount of memory consumption and query calls to a database.

  2. Yes, List Loads all record that meet criteria when you call that method.

  3. Yes, if you want to iterate through your record that meet some criteria and do some processing job you should use Stream one.

  4. This is the tricky one, somehow No, When you are using WebFlux and other similar approaches for reactive programming, I think it is better to go for the Stream one.

Important note: In case when you said some developers collect the stream into a list before returning a response, They can boost their performance using WebFlux and return Stream itself. it is much better approach.

Related