Use case:
I'm building an importer for a CSV file, I have an @Controller
to upload the CSV file and @Entity
's for the CSV file and the lines in the CSV file.
As the CSV files can get quite big I want to import them @Async
.
Problem
When I upload a file, I save it to the database and trigger my async function to process it, this works. Then I process each line in an own transaction and save it as an Entity with its line number to the database, this also works fine. Then i trigger the actual business logic to process the line from the CSV line, which is also executed in its own transaction.
In this last transaction I get a LazyInitializationException
from a repository that is used in that class and I don't know why.
In the catch block of the caller I use the exception message and save it to the database, this works as desired in its own transaction. My upload page immediately returns, the CSV file is processed asynchronously, I get the error for every line in the CSV file, which are saved to the database.
Afterwards I have everything in the database as it should (not) be.
Questions / Thoughts
The repository seems to use another transaction, so it's not using the transaction from the Importer
class, why?
This is what the final class looks like where the LazyInitializationException
is thrown, calling classes are documented below.
@Service
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public class Importer {
private final PersonRepository personRepository;
public Importer(PersonRepository personRepository) {
this.personRepository = personRepository;
}
void import(String[] importLine) {
getPersonAddress(importLine);
}
private PersonAddress getPerson(String[] importLine) {
String name = importLine[5];
Person person = personRepository.findByName(name);
// This results in the following exception:
// org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: de.twimbee.Person.addresses, could not initialize proxy - no Session
return person.getAddresses().get(1);
}
}
Other Classes
the controller used to upload the file and save it to the database.
@Controller
@Transactional(rollbackFor = Exception.class)
public class MyController {
private final CsvFileRepository csvFileRepository;
private final CsvImporter csvImporter;
public MyController(CsvFileRepository csvFileRepository, CsvImporter csvImporter) {
this.csvFileRepository = csvFileRepository;
this.csvImporter = csvImporter;
}
public String postFile(String filename) {
CsvFile csvFile = new CsvFile(filename)
csvFile = csvFileRepository.save(new CsvFile(filename));
try {
List<String[]> csvContent = readCsvContent(filename);
csvImporter.import(csvFile.getId(), csvContent);
} catch (IOException e) {
csvFile.setError(e.getMessage());
}
}
}
The service that solely exists for spring to be able to intercept the @Async
@Service
public class CsvImporter {
private final LineImporter lineImporter;
public CsvImporter(LineImporter lineImporter) {
this.lineImporter = lineImporter;
}
@Async
void import(int csvFileId, List<String[]> csvContent) {
for(int i = 0; i < csvContent.size(); i++) {
lineImporter.import(csvFileId, csvContent.get(i), i + 1);
}
}
}
the line importer with REQUIRES_NEW
so that each line is imported in its own transaction.
@Service
@Transactional(rollbackFor = Exception.class)
public class LineImporter {
private final CsvFileRepository csvFileRepository;
private final ImportLineRepository importLineRepository;
private final Importer importer;
public LineImporter(CsvFileRepository csvFileRepository, ImportLineRepository importLineRepository, Importer importer) {
this.csvFileRepository = csvFileRepository;
this.importLineRepository = importLineRepository;
this.importer = importer;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
void import(int csvFileId, String[] csvContent, int lineNumber) {
ImportLine importLine = new ImportLine(lineNumber);
importLine.setCsvFile(csvFileRepository.findOne(csvFileId));
try {
int dataId = importer.import(importLine);
importLine.setDataId(dataId);
} catch (ImportException e) {
importLine.setError(e.getMessage());
}
importLineRepository.save(importLine);
}
}
until this point everything seems to work as desired. The Importer
throws an exception for every line, the error message get written to the database.
the repositories basically all look the same.
@Repository
@Transactional(rollbackFor = Exception.class)
public interface CsvFileRepository extends JpaRepository<CsvFile, Integer> {
}