Issue
I have a problem with accessing data inside a running transaction when the data came from another (supposedly closed) transaction. I have three classes like below, with an entity (called MyEntity) which also has another entity connected via Hibernate mapping called "OtherEntity" which has lazy loading set to true. Notice how I have two transactions:
- One to load a list of entities
- And a new transaction for each new item
However, this fails inside the loop with "No session" even though I have an active transaction inside the method (TransactionSynchronizationManager.isActualTransactionActive is true).
I don't really understand the problem. Seems to me the object which is used by the second transaction(s) "belong" to the first one even though the first transaction was supposed to finish? Maybe its a race condition?
@Service
class ServiceA {
@Autowired
private ServiceB serviceB;
@Autowired
private ServiceC serviceC;
public void test() {
List<MyEntity> allEntities = serviceC.loadAllEntities(); //First transaction ran, getting a list of entities, but due to lazy loading we havent loaded all the data
for(MyEntity i : allEntities) {
serviceB.doOnEach(i); //On each element a new transaction should start
}
}
}
@Service
class ServiceB {
@Transactional
public void doOnEach(MyEntity entity) {
System.out.println(TransactionSynchronizationManager.isActualTransactionActive()); //true, therefore we have an active transaction here
OtherEntity other = entity.getSomeOtherEntity(); //Want to load the "lazy loaded" entity here
//"No Session" exception is thrown here
}
}
@Service
class ServiceC {
@Autowired
private MyRepository myRepository;
@Transactional
public List<MyEntity> loadAllEntities() {
return myRepository.findAll();
}
}
A solution would be to re-load the "MyEntity" instance inside the "doOnEach" method, but that seems to me like a sub-optimal solution, especially on big lists. Why would I reload all the data which is already supposed to be there?
Any help is appreciated.
Obviously the real code is a lot more complicated than this but I have to have these kind of separate transactions for business reasons, so please no "solutions" which re-write the core logic of this. I just want to understand whats going on here.
Solution
After the call to loadAllEntities()
finishes the Spring proxy commits the transaction and closes the associated Hibernate Session. This means you cannot have Hibernate transparently load the non-loaded lazy associations anymore without explicitly telling it to do so.
If for some reason you really want your associated entities to be loaded lazily the two options you have is either use Hibernate.initialize(entity.getSomeOtherEntity())
in your doOnEach()
method or set the spring.jpa.open-in-view
property to true to have the OpenSessionInViewInterceptor do it for you.
Otherwise it's a good idea to load them together with the parent entity either via JOIN FETCH
in your repository query or via an Entity Graph.
References:
- https://www.baeldung.com/spring-open-session-in-view
- https://www.baeldung.com/hibernate-initialize-proxy-exception
To clarify further:
Spring creates a transaction and opens a new Session (A) before entering the loadAllEntities()
method and commits/closes them upon returning. When you call entity.getSomeOtherEntity()
the original Session (A) that loaded entity
is gone (i.e. entity
is detached) but instead there's a new Session (B) which was created upon entering the doOnEach()
transactional method. Obviously Session (B) doesn't know anything about entity
and its relations and at the same time the Hibernate proxy of someOtherEntity
inside entity
references the original Session (A) and doesn't know anything about Session (B). To make the Hibernate proxy of someOtherEntity
actually use the current active Session (B) you can call Hibernate.initialize()
.
Answered By - dekkard
Answer Checked By - Marilyn (JavaFixing Volunteer)