Search code examples
javaspringhibernatejpaspring-transactions

How do I use multiple @Transactional annotated methods?


I have a Spring-boot project where I have a service bean with 2 @Transactional annotated methods.

These methods do read-only JPA (hibernated) actions to fetch data from an HSQL file database, using both JPA repositories and lazy loaded getters in entities.

I also have a cli bean that handles commands (Using PicoCLI). From one of these commands I try to call both @Transactional annotated methods, but I get the following error during execution of the second method:

org.hibernate.LazyInitializationException - could not initialize proxy - no Session
        at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602)
        at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217)
        at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:581)
        at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:148)
        at org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:188)
        at java.util.Spliterators$IteratorSpliterator.estimateSize(Spliterators.java:1821)
        at java.util.Spliterator.getExactSizeIfKnown(Spliterator.java:408)
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
        at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:566)
        at <mypackage>.SomeImpl.getThings(SomeImpl.java:<linenr>)
...

If I mark the method that calls both @Transactional annotated methods with @Transactional itself, the code seems to work (due to there now only being 1 top level transaction I presume?).

I just want to find out why I cannot start multiple transactions in a single session or why the second transaction doesn't start a new session if there are none.

So my questions are:

  • Does this have to do with how hibernate starts a session, how transactions close sessions or anything related to the HSQL database?
  • Is adding an encompassing transaction the right way to fix the issue or is this just fighting the symptom?
  • What would be the best way to be able to use multiple @Transactional annotated methods from one method?

EDIT: I want to make clear that I don't expose the entities outside of the transactional methods, so on the surface it looks to me like the 2 transactional methods should be working independently from one another.

EDIT2: for more clarification: the transactional methods need to be available in an api and the user of the api should be able to call multiple of these transactional methods, without needing to use transactional annotations and without getting the LazyInitializationException

Api:

public interface SomeApi {
    List<String> getSomeList();
    List<Something> getThings(String somethingGroupName);
}

Implementation:

public class SomeImpl implements SomeApi {

    @Transactional
    public List<String> getSomeList() {
        return ...; //Do jpa stuff to get the list
    }

    @Transactional
    public List<Something> getThings(String somethingGroupName) {
        return ...; //Do other jpa stuff to get the result from the group name
    }
}

Usage by 3rd party (who might not know what transactionality is):

public someMethod(String somethingGroupName) {
    ...

    SomeApi someApi = ...; // Get an implementation of the api in some way

    List<String> someList = someApi.someList();
    if (someList.contains(somethingGroupName) {
        System.out.println(someApi.getThings(somethingGroupName));
    }

    ...
}

Solution

  • I found that hibernate out of the box doesn't reopen a session and therefore doesn't enable lazy loading after the first transaction has ended, whether or not subsequent jpa statements are in a transaction or not. There is however a property in hibernate to enable this feature:

    spring:
      jpa:
        properties:
          hibernate.enable_lazy_load_no_trans: true
    

    This will make sure that if there is no session, then a temp session will be created. I believe that it will also prevent a session from ending after a transaction, but I don't know this for sure.

    Partial credit goes to the following answers from other StackOverflow questions:

    WARNING: In hibernate 4.1.8 there is a bug that could lead to loss of data! Make sure that you are using 4.2.12, 4.3.5 or newer versions of hibernate. See: https://hibernate.atlassian.net/browse/HHH-7971.