We use Hibernate (with JPA) and Hibernate Envers to persist history of objects. The web application runs many threads, some of them are created by RMI method invocation from other applications, some of them are created by the application itself and some of them are created to handle http requests (they generate views).
We also use the Open Session In View pattern to manage sessions, so our web.xml contains:
<filter>
<filter-name>openEntityManagerInViewFilter</filter-name>
<filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>openEntityManagerInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Database is accessed using DAOs, all of them have EntityManagers injected by Spring.
@PersistenceContext
protected EntityManager em;
@PersistenceUnit
protected EntityManagerFactory emf;
Everything worked quite well before we decided to use Hibernate Envers. When any thread that is not a view-generating thread runs the code to get an old version of an object, the exception is thrown.
@Override
public O loadByRevision(Long revision, Long id) {
@SuppressWarnings("unchecked")
O object = (O) AuditReaderFactory.get(em).createQuery().forEntitiesAtRevision(getBaseClass(), revision.intValue())
.add(AuditEntity.id().eq(id)).getSingleResult();
return object;
}
Exception in thread "Scheduler" org.hibernate.SessionException: Session is closed! at org.hibernate.internal.AbstractSessionImpl.errorIfClosed(AbstractSessionImpl.java:129) at org.hibernate.internal.SessionImpl.createQuery(SessionImpl.java:1776) at org.hibernate.envers.tools.query.QueryBuilder.toQuery(QueryBuilder.java:226) at org.hibernate.envers.query.impl.AbstractAuditQuery.buildQuery(AbstractAuditQuery.java:92) at org.hibernate.envers.query.impl.EntitiesAtRevisionQuery.list(EntitiesAtRevisionQuery.java:108) at org.hibernate.envers.query.impl.AbstractAuditQuery.getSingleResult(AbstractAuditQuery.java:110) (...)
When the code above is run by the view-generating thread it works fine. Also, the non-envers code in DAO works fine for every thread. For example, the snippet below
@Override
public O load(Long id) {
final O find = em.find(getBaseClass(), id);
return find;
}
can be run by RMI threads without problems.
Why can non-view threads call methods on the entity manager without exceptions but cannot use Envers' AuditReaderFactory with that entity manager? I thought that maybe calling a method on the entity manager creates a temporary session but that doesn't happen when using Envers, is that true?
What is the best way to fix that issue (so that the AuditReaderFactory can be used from every thread)?
We didn't find out why in non-ui threads method calls on EntityManagerFactory
worked but method calls on AuditReaderFactory
didn't. Anyway, we found a way to fix it.
The solution was to annotate methods with @Transactional
. If any method in the call chain before call to the AuditReaderFactory was marked as @Transactional
, there was no SessionException
in non-ui threads.
It turned out that making loadByRevision
transactional was not sufficient. If an object returned by that method contained lazy-loaded persistent bags, access to them outside the loadByRevision
method scope caused LazyInitializationException
(there was no session).
The final solution was to make sure that if any thread wants to load some data from the database, all the loading (getting an object and accessing lazy-loaded collections) will be done inside one method annotated with @Transactional
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With