0

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> {
}
Stefan Ortgies
  • 295
  • 1
  • 2
  • 13
  • Can you post code for you address entity and person entity. It seems you have @OneToOne(fetch=FetchType.LAZY) instead of @OneToOne(fetch=FetchType.EAGER) in your person entity wherever you are establishing relation between address and person – user8271644 Jul 19 '17 at 16:01
  • @user8271644 It is a @OneToMany(fetch=FetchType.LAZY), you are correct that EAGER would help, but that is not what I want, I want the data to be lazily fetched in the same transaction. – Stefan Ortgies Jul 20 '17 at 07:37

1 Answers1

1
@Transactional(propagation = Propagation.REQUIRES_NEW)
void import(int csvFileId, String[] csvContent, int lineNumber) {

By default @Transactional works only on public methods

To change that u need to switch from Spring AOP Proxt to AspecJ Proxy

You can find more details here Spring AOP vs AspectJ

Bartun
  • 549
  • 2
  • 12