'Creating JPA session for background threads
We use Hibernate through JPA and Spring to manage object persistence in our web application. We use open-session-in-view pattern to create sessions for threads responding to http requests. We also use some threads that are not generating views - they just wake up from time to time to do their job. That generates problems because they don't have session opened by default so they generate exceptions like
org.hibernate.SessionException: Session is closed!
or
could not initialize proxy - no Session
We found out that if every background thread invokes its logic in a method annotated with @Transactional
there are no exceptions of this kind as @Transactional
makes sure that thread has session when it's inside the transaction.
It solved problems for some time but I don't think that it's a good solution - making long-running methods transactional causes problems because other threads can't see changes made in database until the transaction is committed.
I created a java-pseudocode example to better illustrate my problem:
public class FirstThread {
...
@Transactional
public void processQueue() {
for(element : queue){
if(elementCanBeProcessed(element)){
elementDao.saveIntoDatabase(element);
secondThread.addToQueue(element.getId());
}
}
}
private boolean elementCanBeProcessed(element){
//code that gets a few objects from database and processes them
}
}
If I annotate the whole processQueue
method with @Transactional
the changes made in
elementDao.saveIntoDatabase(element);
won't be seen in secondThread
until the transaction is committed (so until the whole queue is processed). If I don't do that then the thread won't have session inside the elementCanBeProcessed
and it won't be able to access the database. I also can't annotate elementCanBeProcessed
instead because it's a private method in this class and I would have to move this into another class so that Spring proxy could work.
Is it possible to bind session to thread without making the whole method transactional? How should I manage sessions and transactions in background threads like that one?
Solution 1:[1]
I do not know of any Spring-ready solution for this. So, I think you need to implement one similar to the OpenEntityManagerInViewInterceptor class.
Basically, you need to use TransactionSynchronizationManager to bindResource() an instance of EntityManagerHolder for your thread when it is started and unbindResource() when your thread is finished.
The core part of OpenEntityManagerInViewInterceptor is:
if (TransactionSynchronizationManager.hasResource(getEntityManagerFactory())) {
...
}
else {
logger.debug("Opening JPA EntityManager in OpenEntityManagerInViewInterceptor");
try {
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(getEntityManagerFactory(), emHolder);
...
}
catch (PersistenceException ex) {
throw new DataAccessResourceFailureException("Could not create JPA EntityManager", ex);
}
}
Please post the code here as an answer if you implemented it.
Solution 2:[2]
Here is the code I wrote after reading Amir Moghimi's answer. It seems a little bit 'hacky' because the documentation says that neither EntityManagerHolder nor TransactionSynchronizationManager should be used directly by a typical application code.
@Service
public class DatabaseSessionManager {
@PersistenceUnit
private EntityManagerFactory entityManagerFactory;
public void bindSession() {
if (!TransactionSynchronizationManager.hasResource(entityManagerFactory)) {
EntityManager entityManager = entityManagerFactory.createEntityManager();
TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager));
}
}
public void unbindSession() {
EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager
.unbindResource(entityManagerFactory);
EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager());
}
}
It seems to be working - the session is bound to my thread between bindSession()
and unbindSession()
calls and I don't have to create a transaction to achieve it.
Solution 3:[3]
If you want each element to be processed in its own transaction you need to:
- Remove
@Transactional
fromprocessQueue
- Move the
elementCanBeProcessed
logic into a separateElementProcessor @Component
class and annotate theelementCanBeProcessed
with@Transactional
- Call
elementProcessor.elementCanBeProcessed(element);
Because the elementProcessor
is a separate bean, it gets proxied by Spring, therefore calls will be intercepted by the TransactionInterceptor
.
Solution 4:[4]
Avoid long-running transactions across task, so that transactions are small and less likely to conflict/rollback. Here is what I did relying on JPA without JTA and @Transactional:
Inject entity manager or get it from Spring context or pass it as reference:
@PersistenceContext
private EntityManager entityManager;
Then create a new entity manager, to avoid using a shared one:
EntityManager em = entityManager.getEntityManagerFactory().createEntityManager();
Now you can start transaction and use Spring DAO, Repository, JPA, etc
private void save(EntityManager em) {
try
{
em.getTransaction().begin();
<your database changes>
em.getTransaction().commit();
}
catch(Throwable th) {
em.getTransaction().rollback();
throw th;
}
}
You will also need to provide JPA beans in your database @Configuration, but most likely you already have that.
Solution 5:[5]
Building upon Amir's and Pawel's answers and the JpaTemplate idea, I created a component to fulfill the template role:
@Component
@Slf4j
public class EntityManagerProvider {
private final EntityManagerFactory entityManagerFactory;
public EntityManagerProvider(EntityManagerFactory entityManagerFactory) {
this.entityManagerFactory = entityManagerFactory;
}
/**
* Attaches a JPA {@link EntityManager} to the current thread (if none is already present).
* For use within Springboot background threads (such as ones associated with a scheduler),
* when we don't want to enclose all work within a single transaction.
*
* @param operation lambda that needs to run with a JPA session available.
*/
public void withEntityManager(Operation operation) {
if (isEntityManagerBoundToThread()) {
operation.get();
} else {
withNewEntityManager(operation);
}
}
@FunctionalInterface
public interface Operation {
void get();
}
private boolean isEntityManagerBoundToThread() {
return TransactionSynchronizationManager.hasResource(entityManagerFactory);
}
private void withNewEntityManager(Operation operation) {
try {
bindEntityManager();
operation.get();
} finally {
unbindEntityManager();
}
}
private void bindEntityManager() {
EntityManager entityManager = entityManagerFactory.createEntityManager();
TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager));
}
private void unbindEntityManager() {
try {
EntityManagerHolder holder =
(EntityManagerHolder) TransactionSynchronizationManager.unbindResource(entityManagerFactory);
EntityManagerFactoryUtils.closeEntityManager(holder.getEntityManager());
} catch (Throwable t) {
log.error("Failed to unbind EntityManager", t);
}
}
}
This is then used from the calling code like this:
entityManagerProvider.withEntityManager(() -> {
... code using JPA operations ...
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Amir Moghimi |
Solution 2 | Community |
Solution 3 | naXa stands with Ukraine |
Solution 4 | adlerer |
Solution 5 | Reg Whitton |