Como faço corretamente um thread em segundo plano ao usar Spring Data e Hibernate?

Eu estou construindo uma aplicação web simples Tomcat que está usando Spring Data e Hibernate. Há um ponto final que faz muito trabalho, portanto, quero descarregar o trabalho em um thread de segundo plano para que a solicitação da Web não seja interrompida por mais de 10 minutos enquanto o trabalho está sendo executado. Então eu escrevi um novo serviço em um pacote component-scan’d:

@Service public class BackgroundJobService { @Autowired private ThreadPoolTaskExecutor threadPoolTaskExecutor; public void startJob(Runnable runnable) { threadPoolTaskExecutor.execute(runnable); } } 

Em seguida, o ThreadPoolTaskExecutor configurado no Spring:

      

Isso tudo está funcionando muito bem. No entanto, o problema vem do Hibernate. Dentro do meu runnable, consultas apenas metade do trabalho. Eu posso fazer:

 MyObject myObject = myObjectRepository.findOne() myObject.setSomething("something"); myObjectRepository.save(myObject); 

Mas se eu tenho campos carregados preguiçosos, ele falha:

 MyObject myObject = myObjectRepository.findOne() List lazies = myObject.getLazies(); for(Lazy lazy : lazies) { // Exception ... } 

Estou tendo o erro a seguir:

 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.stackoverflow.MyObject.lazies, could not initialize proxy - no Session 

Então, parece-me (newbie do Hibernate) que o novo thread não tem uma session sobre esses threads caseiros, mas o Spring Data está criando automaticamente novas sessões para threads de solicitação de HTTP.

  • Existe uma maneira de iniciar uma nova session manualmente a partir da session?
  • Ou uma maneira de dizer ao pool de threads para fazer isso por mim?
  • Qual é a prática padrão para fazer esse tipo de trabalho?

Eu pude trabalhar um pouco com isso, fazendo tudo de dentro de um método @Transactional , mas eu estou rapidamente aprendendo que não é uma solução muito boa, já que isso não me deixa usar methods que funcionam muito bem para solicitações da web .

Obrigado.

Com Spring você não precisa do seu próprio executor. Uma simples anotação @Async fará o trabalho para você. Apenas anote seu heavyMethod em seu serviço com ele e retorne um object void ou Future e você obterá um thread em segundo plano. Eu evitaria usar a anotação assíncrona no nível do controlador, pois isso criaria um encadeamento asynchronous no executor do conjunto de solicitações e você poderia ficar sem ‘requisitadores de solicitação’.

O problema com sua exceção preguiçosa vem como você suspeitou do novo segmento que não tem uma session. Para evitar esse problema, seu método asynchronous deve lidar com o trabalho completo. Não forneça entidades carregadas anteriormente como parâmetros. O serviço pode usar um EntityManager e também pode ser transacional.

Eu para mim não mesclar @Async e @Transactional para que eu possa executar o serviço de qualquer maneira. Acabei de criar um wrapper asynchronous em torno do serviço e usá-lo, se necessário. (Isso simplifica o teste, por exemplo)

 @Service public class AsyncService { @Autowired private Service service; @Async public void doAsync(int entityId) { service.doHeavy(entityId); } } @Service public class Service { @PersistenceContext private EntityManager em; @Transactional public void doHeavy(int entityId) { // some long running work } } 

O que acontece é, provavelmente, você tem transação em seu pedaço de código DAO e Spring está fechando a session no fechamento da transação.

Você deve espremer toda a sua lógica de negócios em transação única.

Você pode injetar SessionFactory em seu código e usar o método SessionFactory.openSession() .
O problema é que você terá que gerenciar suas transactions.

Método # 1: Gerenciador de Entidade JPA

No thread de segundo plano: Injectar o gerenciador de entidades ou obtê-lo do contexto Spring ou passá-lo como referência:

 @PersistenceContext private EntityManager entityManager; 

Em seguida, crie um novo gerenciador de entidades para evitar o uso de um gerenciador de entidades compartilhado:

 EntityManager em = entityManager.getEntityManagerFactory().createEntityManager(); 

Agora você pode iniciar transactions e usar Spring DAO, Repository, JPA, etc

 private void save(EntityManager em) { try { em.getTransaction().begin();  em.getTransaction().commit(); } catch(Throwable th) { em.getTransaction().rollback(); throw th; } } 

Método # 2: JdbcTemplate

Caso você precise de alterações de baixo nível ou sua tarefa seja simples o suficiente, você pode fazê-lo com o JDBC e consultar manualmente:

 @Autowired private JdbcTemplate jdbcTemplate; 

e depois em algum lugar no seu método:

 jdbcTemplate.update("update task set `status`=? where id = ?", task.getStatus(), task.getId()); 

Nota: Eu recomendaria ficar longe da @Transactional, a menos que você use JTA ou dependa do JpaTransactionManager.