Rozumiem problem. Kluczowa kwestia: Oracle JDBC nie pozwala na równoczesne operacje LOB na tym samym połączeniu, a REQUIRES_NEW w Spring domyślnie nie gwarantuje nowego połączenia — tylko nową transakcję logiczną.
Masz mniej więcej taki flow:
- Metoda A (
@Transactional) — otwiera transakcję, pobiera CLOB jakoInputStream, parsuje StAX-em - W trakcie parsowania wywołujesz Metodę B (
@Transactional(REQUIRES_NEW)) — Spring zawiesza transakcję A, otwiera nową transakcję B - Metoda B robi
saveAll()— Hibernate flushuje encje i pod spodem próbuje użyć tego samego połączenia JDBC - Oracle widzi, że na tym połączeniu wciąż jest otwarty LOB read stream → rzuca ORA-17453
REQUIRES_NEW tworzy nową transakcję w sensie begin/commit, ale Hibernate/Spring mogą (i zazwyczaj tak robią) użyć tego samego połączenia z puli, bo jest ono przypisane do bieżącego wątku. Nawet jeśli Spring weźmie nowe połączenie, sterownik Oracle może mieć problem, jeśli LOB locator jest nadal powiązany z sesją.
Nie czytaj CLOB-a jako InputStream w trakcie wywoływania zapisu. Zamiast tego wczytaj cały XML do String lub byte[] jeszcze przed parsowaniem:
@Transactional(readOnly = true)
public void processClob(Long id) {
// Zmaterializuj CLOB do Stringa — LOB read kończy się tutaj
String xml = repository.findClobAsString(id);
// Teraz parsuj StAX-em z StringReadera — żadnego otwartego LOB-a
try (var reader = XMLInputFactory.newInstance()
.createXMLStreamReader(new StringReader(xml))) {
List<Transaction> batch = new ArrayList<>(500);
while (reader.hasNext()) {
// ... parsowanie ...
batch.add(transaction);
if (batch.size() == 500) {
transactionSaver.saveBatch(new ArrayList<>(batch));
batch.clear();
}
}
if (!batch.isEmpty()) {
transactionSaver.saveBatch(batch);
}
}
}W repozytorium:
@Query("SELECT CAST(e.xmlData AS string) FROM MyEntity e WHERE e.id = :id")
String findClobAsString(@Param("id") Long id);Albo natywnie:
@Query(value = "SELECT DBMS_LOB.SUBSTR(xml_data, DBMS_LOB.GETLENGTH(xml_data), 1) FROM my_table WHERE id = :id",
nativeQuery = true)
String findClobAsString(@Param("id") Long id);Uwaga: Jeśli CLOB jest naprawdę duży (setki MB), to rozwiązanie może być problematyczne pamięciowo. Dla większości przypadków (do kilkudziesięciu MB) jest to najprostsza i najskuteczniejsza opcja.
Jeśli CLOB jest ogromny i musisz streamować, to potrzebujesz fizycznie osobnego połączenia dla zapisów:
@Service
public class TransactionSaver {
@Autowired
private DataSource dataSource;
@Autowired
private EntityManagerFactory emf;
// NIE używaj @Transactional — zarządzaj ręcznie
public void saveBatch(List<Transaction> batch) {
// Nowy EntityManager = nowe połączenie z puli
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
for (Transaction t : batch) {
em.persist(t);
}
em.flush();
em.getTransaction().commit();
} catch (Exception e) {
em.getTransaction().rollback();
throw e;
} finally {
em.close(); // oddaje połączenie do puli
}
}
}To gwarantuje osobne połączenie JDBC, niezależne od tego, na którym leży otwarty LOB.
@Transactional(readOnly = true)
public List<List<Transaction>> parseAll(Long id) {
// Faza 1: czytaj CLOB, parsuj, zbieraj paczki
// Transakcja (i LOB) zamyka się po wyjściu z tej metody
InputStream stream = repository.getClobStream(id);
return parseIntoBatches(stream, 500);
}
// Wywoływane z zewnątrz, PO zakończeniu parseAll
public void saveAll(List<List<Transaction>> batches) {
for (List<Transaction> batch : batches) {
transactionSaver.saveBatch(batch); // REQUIRES_NEW działa OK
}
}To separuje LOB read od write w czasie — ale wymaga trzymania wszystkich danych w pamięci.
Rozwiązanie 1 (materializacja do String) jest najczystsze i wystarczy w 90% przypadków. Sięgaj po rozwiązanie 2 tylko wtedy, gdy CLOB jest na tyle duży, że nie chcesz go trzymać w pamięci jako String.