Our environment is Spring Boot with Spring Data JPA. I’m dealing with significant legacy code, and we have a generic problem I’m trying to fix.
We have a number of places where a REST call spawns a background thread:
org.hibernate.LazyInitializationException: could not initialize proxy [org.showpage.threadingtransactions.dbmodel.Author#1] - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:165) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:314) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:44) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
at org.showpage.threadingtransactions.dbmodel.Author$HibernateProxy$n6D0Du2E.getName(Unknown Source) ~[classes/:na]
at org.showpage.threadingtransactions.service.SubService.populate(SubService.java:44) ~[classes/:na]
at org.showpage.threadingtransactions.service.BookInfoService.lambda$spawnWillPass$1(BookInfoService.java:115) ~[classes/:na]
at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
This is from a sample project I created to try to work on a general solution, which can be seen at https://github.com/jplflyer/SpringDataThreading.git.
My sample application is very small. I have a WebController.java:
@RestController
@RequiredArgsConstructor
@Slf4j
public class WebController {
private final AuthorRepository authorRepository;
private final BookRepository bookRepository;
private final BookInfoService bookInfoService;
@GetMapping("/request")
public ResponseEntity<String> makeRequest(
@RequestParam(required = false, defaultValue = "false") boolean pass,
@RequestParam(name = "no_spawn", required = false, defaultValue = "false") boolean noSpawn
) {
log.info("makeRequest({}, {})", pass, noSpawn);
BookInfoService.Mode mode = noSpawn
? BookInfoService.Mode.NoSpawn
: ( pass ? BookInfoService.Mode.SpawnWillPass : BookInfoService.Mode.SpawnWillFail);
int requestId = bookInfoService.makeRequest(mode);
return ResponseEntity.ok(String.format("%dn", requestId));
}
}
BookInfoService.java is longer only because I’ve tried a bunch of ways to fix this:
@Service
@RequiredArgsConstructor
@Slf4j
public class BookInfoService {
public enum Mode {
SpawnWillFail,
SpawnWillPass,
NoSpawn
}
private final BookRepository bookRepository;
private final EntityManager entityManager;
private SessionFactory sessionFactory;
private static int requestId = 0;
/**
* Spawn the request.
*/
public int makeRequest(Mode mode) {
BookInfo info = BookInfo
.builder()
.requestId(++requestId)
.build();
switch (mode) {
case SpawnWillFail: spawnWillFail(info); break;
case SpawnWillPass: spawnWillPass(info); break;
case NoSpawn: runNoSpawn(info); break;
}
return info.getRequestId();
}
/**
* This version to prove it works if we don't run worker threads.
*/
private void runNoSpawn(BookInfo info) {
SubService subService = new SubService(entityManager, bookRepository);
try {
subService.populate(info);
}
catch (Exception e) {
log.error("Exception", e);
}
}
/**
* This version will fail with a lazy initialization exception.
*/
private void spawnWillFail(BookInfo info) {
List<Book> allBooks = bookRepository.findAll();
new Thread(() -> {
log.info("Bad Thread start");
populate(info, allBooks);
log.info("Bad Thread done");
}).start();
}
/**
* This version will pass once I get it to work but currently fails.
*/
private void spawnWillPass(BookInfo info) {
EntityManager em = entityManager
.getEntityManagerFactory()
.createEntityManager();
SessionFactory sessionFactory = em.getEntityManagerFactory().unwrap(SessionFactory.class);
SubService subService = new SubService(em, bookRepository);
new Thread(() -> {
log.info("Good Thread start");
try {
Session session = sessionFactory.openSession();
em.getTransaction().begin();
subService.populate(info);
em.getTransaction() .commit();
}
catch (Exception e) {
log.error("Exception", e);
em.getTransaction().rollback();
}
finally {
em.close();
sessionFactory.close();
}
log.info("Good Thread done");
}).start();
}
}
SubService.java is boring:
@RequiredArgsConstructor
@Slf4j
@Service
public class SubService {
@PersistenceContext
private final EntityManager em;
private final BookRepository bookRepository;
/**
* Worker for the above.
*/
@Transactional
public void populate(BookInfo info) {
List<Book> allBooks = bookRepository.findAll();
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
StringBuilder sb = new StringBuilder();
try {
for (Book book : allBooks) {
sb.append(
String.format("Name: %s Author: %sn",
book.getName(),
book.getAuthor().getName())
);
}
}
catch (Exception e) {
log.error("Exception", e);
}
info.setData(sb.toString());
info.setDone(true);
}
}
I have absolutely no beans configured, and my application.yml file is only enough to define the connection to the database.
I test this with:
curl -s "http://localhost:8080/request?no_spawn=false&pass=true"
and then I watch the logs in IntelliJ.
Set no_spawn
to true, and you get the don’t thread version. But this call is the one I’m trying to fix — spawning a thread, but it’s the one that actually tries to work.
I know that the @async
flag might be a solution, but as I said — significant legacy code I’m trying to fix. I know I could Fetch-Eager, but that’s a particularly bad solution. I could prefetch All The Data, but that’s a lot of places where I’d have to handle prefetching before spawning.
Currently, we’re fixing these one at a time by using the repository directly instead of traversing the ORM. That is, instead of book.getAuthor().getName()
, I would fetch the author from the AuthorRepository
using book.getAuthorId()
. But again, that involves finding them one by one, and there’s a lot of these.
I’ve read a couple of dozen “solutions”, which is where I found references to trying to use EntityManager
as you see here as well as SessionFactory
. But nothing I’ve done has actually solved the problem.
So… Is there some way I can solve the No Session
failure?