Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data JPA - Java 8 Stream Support & Transactional Best Practices

I have a pretty standard MVC setup with Spring Data JPA Repositories for my DAO layer, a Service layer that handles Transactional concerns and implements business logic, and a view layer that has some lovely REST-based JSON endpoints.

My question is around wholesale adoption of Java 8 Streams into this lovely architecture: If all of my DAOs return Streams, my Services return those same Streams (but do the Transactional work), and my Views act on and process those Streams, then by the time my Views begin working on the Model objects inside my Streams, the transaction created by the Service layer will have been closed. If the underlying data store hasn't yet materialized all of my model objects (it is a Stream after all, as lazy as possible) then my Views will get errors trying to access new results outside of a transaction. Previously this wasn't a problem because I would fully materialize results into a List - but now we're in the brave new world of Streams.

So, what is the best way to handle this? Fully materialize the results inside of the Service layer as a List and hand them back? Have the View layer hand the Service layer a completion block so further processing can be done inside of a transaction?

Thanks for the help!

like image 640
Michael Ressler Avatar asked Apr 20 '16 05:04

Michael Ressler


People also ask

Does Java 8 support streams?

Note that Java 8 added a new stream() method to the Collection interface. And we can create a stream from individual objects using Stream.

Is Stream API introduced in Java 8?

Introduced in Java 8, the Stream API is used to process collections of objects. A stream is a sequence of objects that supports various methods which can be pipelined to produce the desired result.

How many types of streams are available in Java 8?

Java 8 offers the possibility to create streams out of three primitive types: int, long and double. As Stream<T> is a generic interface, and there is no way to use primitives as a type parameter with generics, three new special interfaces were created: IntStream, LongStream, DoubleStream.


1 Answers

In thinking through this, I decided to try the completion block solution I mentioned in my question. All of my service methods now have as their final parameter a results transformer that takes the Stream of Model objects and transforms it into whatever resulting type is needed/requested by the View layer. I'm pleased to report it works like a charm and has some nice side-effects.

Here's my Service base class:

public class ReadOnlyServiceImpl<MODEL extends AbstractSyncableEntity, DAO extends AbstractSyncableDAO<MODEL>> implements ReadOnlyService<MODEL> {

    @Autowired
    protected DAO entityDAO;

    protected <S> S resultsTransformer(Supplier<Stream<MODEL>> resultsSupplier, Function<Stream<MODEL>, S> resultsTransform) {
        try (Stream<MODEL> results = resultsSupplier.get()) {
            return resultsTransform.apply(results);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public <S> S getAll(Function<Stream<MODEL>, S> resultsTransform) {
        return resultsTransformer(entityDAO::findAll, resultsTransform);
    }

}

The resultsTransformer method here is a gentle reminder for subclasses to not forget about the try-with-resources pattern.

And here is an example Controller calling in to the service base class:

public abstract class AbstractReadOnlyController<MODEL extends AbstractSyncableEntity, 
                                                 DTO extends AbstractSyncableDTOV2, 
                                                 SERVICE extends ReadOnlyService<MODEL>> 
{

    @Autowired
    protected SERVICE entityService;

    protected Function<MODEL, DTO> modelToDTO;

    protected AbstractReadOnlyController(Function<MODEL, DTO> modelToDTO) {
        this.modelToDTO = modelToDTO;
    }

    protected List<DTO> modelStreamToDTOList(Stream<MODEL> s) {
        return s.map(modelToDTO).collect(Collectors.toList());
    }

    // Read All
    protected List<DTO> getAll(Optional<String> lastUpdate) 
    {
        if (!lastUpdate.isPresent()) {
            return entityService.getAll(this::modelStreamToDTOList);
        } else {
            Date since = new TimeUtility(lastUpdate.get()).getTime();
            return entityService.getAllUpdatedSince(since, this::modelStreamToDTOList);
        }
    }
}

I think it's a pretty neat use of generics to have the Controllers dictate the return type of the Services via the Java 8 lambda's. While it's strange for me to see the Controller directly returning the result of a Service call, I do appreciate how tight and expressive this code is.

I'd say this is a net positive for attempting a wholesale switch to Java 8 Streams. Hopefully this helps someone with a similar question down the road.

like image 113
Michael Ressler Avatar answered Sep 28 '22 16:09

Michael Ressler