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