Search code examples
spring-boothibernatesessionentitymanagerlazy-initialization

Hibernate LazyInitialization exception in console Spring Boot with an open session


I'm not sure if anyone has experienced this particular twist of the LazyInitialization issue. I have a console Spring Boot application (so: no views - everything basically happens within a single execution method). I have the standard beans and bean repositories, and the typical lazy relationship in one of the beans, and it turns out that unless I specify @Transactional, any access to any lazy collection automatically fails even though the session stays the same, is available, and is open. In fact, I can happily do any session-based operation as long as I don't try to access a lazy collection.

Here's a more detailed example :

@Entity
class Path  { 
    ...    `  
    @OneToMany(mappedBy = "path",fetch = FetchType.LAZY,cascade = CascadeType.ALL)
    @OrderBy("projectOrder")  `  
    public List<Project> getProjects() {
       return projects;    `
   }` 
}

Now, the main method does something as simple as this:

class SpringTest {    
   ...    `  
  @Autowired  
  private PathRepository pathRepository;  
  
  void foo() {    
      Path path = pathRepository.findByNameKey("...");    
      System.out.println(path.getProjects());  // Boom <- Lazy Initialization exception}  
}

Of course if I slap a @Transactional on top of the method or class it works, but the point is - why should I need that? No one is closing the session, so why is Hibernate complaining that there´s no session when there is one?

In fact, If I do:

void foo() {
  System.out.println(entityManager.unwrap(Session.class));
  System.out.println(entityManager.unwrap(Session.class).isOpen());
  Path basic = pathRepository.findByNameKey("...");
  System.out.println(entityManager.unwrap(Session.class));
  System.out.println(entityManager.unwrap(Session.class).isOpen());
  System.out.println(((AbstractPersistentCollection)basic.projects).getSession());
  Path p1 = pathRepository.findByNameKey("....");
}

I get that the session object stays the same the whole time, it stays open the whole time, but the internal session property of the collection is never set to anything other than null, so of course when Hibernate tries to read that collection, in its withTemporarySessionIfNeeded method it immediately throws an exception

private <T> T withTemporarySessionIfNeeded(LazyInitializationWork<T> lazyInitializationWork) {
    SharedSessionContractImplementor tempSession = null;
    if (this.session == null) {
        if (this.allowLoadOutsideTransaction) {
            tempSession = this.openTemporarySessionForLoading();
        } else {
            this.throwLazyInitializationException("could not initialize proxy - no Session");
        }

So I guess my question would be - why is this happening? Why doesn't Hibernate store or access the session from which a bean was fetched so that it can load the lazy collection from it?

Digging a bit deeper, it turns out that the repository method executying the query does a

// method here is java.lang.Object org.hibernate.query.Query.getSingleResult()
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   ...
   if (SharedEntityManagerCreator.queryTerminatingMethods.contains(method.getName())) {
     ...

     EntityManagerFactoryUtils.closeEntityManager(this.entityManager);  // <--- Why?!?!?
     this.entityManager = null;
  }
...
}

and the above closeEntityManager calls unsetSession on all collections:

SharedSessionContractImplementor session = this.getSession();
if (this.collectionEntries != null) {
    IdentityMap.onEachKey(this.collectionEntries, (k) -> {
        k.unsetSession(session);
    });
}

But why?!

(Spring Boot version is 2.7.8)


Solution

  • So, after researching more it appears that this is standard behavior in Spring Boot - unless you use your own EntityManager, the one managed automatically by Spring is either attached to a @Transactional boundary, or opens and closes for each query.

    Some relevant links: Does Entity manager needs to be closed every query?

    Do I have to close() every EntityManager?

    In the end, I ended using a TransactionTemplate to wrap my code into a transaction without having to mark the whole class @Transactional.