This can also be accomplished with Spring MVC Controller, but there are a few concerns: limitations in Spring Data JPA Repository, whether the database supports Holdable Cursors (ResultSet Holdability) and the version of Jackson.
The key concept, I struggled to appreciate, is that a Java 8 Stream returns a series of functions which execute in a terminal operation, and therefore the database has to be accessible in the context executing the terminal operation.
Spring Data JPA Limitations
I found the Spring Data JPA documentation does not provide enough detail for Java 8 Streams. It looks like you can simply declare Stream<MyObject> readAll(), but I needed to annotate the method with @Query to make it work. I was also not able to use a JPA criteria API Specification. So I had to settle for a hard-coded query like:
@Query("select mo from MyObject mo where mo.foo.id in :fooIds")
Stream<MyObject> readAllByFooIn(@Param("fooIds") Long[] fooIds);
Holdable Cursor
If you have a database supporting Holdable Cursors, the result set is accessible after the transaction is committed. This is important since we typically annotate our @Service class methods with @Transactional, so if your database supports holdable cursors the ResultSet can be accessed after the service method returns, i.e. in the @Controller method. If the database does not support holdable cursors, e.g. MySQL, you'll need to add the @Transaction annotation to the controller's @RequestMapping method.
So now the ResultSet is accessible outside the @Service method, right? That again depends on holdability. For MySQL, it's only accessible within the @Transactional method, so the following will work (though defeats the whole purpose of using Java 8 Streams):
@Transaction @RequestMapping(...)
public List<MyObject> getAll() {
try(Stream<MyObject> stream = service.streamAll) {
return stream.collect(Collectors.toList())
};
}
but not
@Transaction @RequestMapping
public Stream<MyObject> getAll() {
return service.streamAll;
}
because the terminal operator is not in your @Controller it happens in Spring after the controller method returns.
Serializing a stream to JSON without Holdable Cursor support
To serialize the stream to JSON without a holdable cursor, add HttpServletResponse response to the controller method, get the output stream and use ObjectMapper to write the stream. With FasterXML 3.x, you can call ObjectMapper().writeValue(writer, stream), but with 2.8.x you have to use the stream's iterator:
@RequestMapping(...)
@Transactional
public void getAll(HttpServletResponse response) throws IOException {
try(final Stream<MyObject> stream = service.streamAll()) {
final Writer writer = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
new ObjectMapper().writerFor(Iterator.class).writeValue(writer, stream.iterator());
}
}
Next steps
My next steps are to attempt refactor this within a Callable WebAsyncTask and to move the JSON serialization into a service.
References