Skip to content

Instantly share code, notes, and snippets.

@lmonkiewicz
Last active March 30, 2026 12:29
Show Gist options
  • Select an option

  • Save lmonkiewicz/d0b70015ae4f7f68ca8958b01d7ebccf to your computer and use it in GitHub Desktop.

Select an option

Save lmonkiewicz/d0b70015ae4f7f68ca8958b01d7ebccf to your computer and use it in GitHub Desktop.
lob-problem

Problem z LOB i zagnieżdżonymi transakcjami w Hibernate/Oracle

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ą.

Co się dzieje krok po kroku

Masz mniej więcej taki flow:

  1. Metoda A (@Transactional) — otwiera transakcję, pobiera CLOB jako InputStream, parsuje StAX-em
  2. W trakcie parsowania wywołujesz Metodę B (@Transactional(REQUIRES_NEW)) — Spring zawiesza transakcję A, otwiera nową transakcję B
  3. Metoda B robi saveAll() — Hibernate flushuje encje i pod spodem próbuje użyć tego samego połączenia JDBC
  4. 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ą.

Jak to naprawić

Rozwiązanie 1: Zmaterializuj dane przed zagnieżdżoną transakcją (najprostsze)

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.

Rozwiązanie 2: Wymuś osobne połączenie dla zapisu

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.

Rozwiązanie 3: Dwufazowe przetwarzanie — najpierw czytaj, potem zapisuj

@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.

Które rozwiązanie wybrać?

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.

Tak, jak najbardziej. TransactionTemplate to nawet czystsze podejście niż ręczne begin/commit/rollback, ale sam w sobie nie rozwiązuje problemu — bo domyślnie użyje tego samego połączenia powiązanego z wątkiem.

Musisz go połączyć z osobnym EntityManager (lub osobnym DataSource/PlatformTransactionManager), żeby wymusić fizycznie inne połączenie.

Wariant A: TransactionTemplate + ręczny EntityManager

@Service
public class TransactionSaver {

    private final EntityManagerFactory emf;
    private final JpaTransactionManager separateTxManager;

    public TransactionSaver(EntityManagerFactory emf, DataSource dataSource) {
        this.emf = emf;
        // Osobny TransactionManager — nie ten główny zarządzany przez Spring
        this.separateTxManager = new JpaTransactionManager(emf);
    }

    public void saveBatch(List<Transaction> batch) {
        TransactionTemplate txTemplate = new TransactionTemplate(separateTxManager);
        txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

        txTemplate.executeWithoutResult(status -> {
            EntityManager em = emf.createEntityManager();
            try {
                EntityManagerHolder holder = new EntityManagerHolder(em);
                TransactionSynchronizationManager
                    .bindResource(emf, holder);

                for (Transaction t : batch) {
                    em.persist(t);
                }
                em.flush();
            } finally {
                TransactionSynchronizationManager.unbindResource(emf);
                em.close();
            }
        });
    }
}

To dość dużo boilerplate'u. Prostszy wariant:

Wariant B: TransactionTemplate + osobny DataSource (czystsze)

Zdefiniuj drugi DataSource i TransactionManager dedykowany do zapisów:

@Configuration
public class WriterDataSourceConfig {

    @Bean
    @Qualifier("writerTxManager")
    public PlatformTransactionManager writerTxManager(DataSource dataSource) {
        // Ten sam DataSource, ale Spring stworzy osobny TX context
        return new DataSourceTransactionManager(dataSource);
    }
}
@Service
public class TransactionSaver {

    private final TransactionTemplate txTemplate;
    private final JdbcTemplate jdbc;

    public TransactionSaver(
            @Qualifier("writerTxManager") PlatformTransactionManager txManager,
            DataSource dataSource) {
        this.txTemplate = new TransactionTemplate(txManager);
        this.txTemplate.setPropagationBehavior(
            TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        this.jdbc = new JdbcTemplate(dataSource);
    }

    public void saveBatch(List<Transaction> batch) {
        txTemplate.executeWithoutResult(status -> {
            jdbc.batchUpdate(
                "INSERT INTO transactions (col1, col2) VALUES (?, ?)",
                new BatchPreparedStatementSetter() {
                    public void setValues(PreparedStatement ps, int i) 
                            throws SQLException {
                        ps.setString(1, batch.get(i).getCol1());
                        ps.setString(2, batch.get(i).getCol2());
                    }
                    public int getBatchSize() { return batch.size(); }
                }
            );
        });
    }
}

Szczera rekomendacja

Jeśli zależy Ci na prostocie i chcesz zostać przy JPA/Hibernate, to wariant z ręcznym EntityManager bez TransactionTemplate (rozwiązanie 2 z poprzedniej odpowiedzi) jest paradoksalnie prostszy. TransactionTemplate błyszczy tam, gdzie chcesz programmatic transactions w ramach standardowego zarządzania połączeniami Springa — a tutaj cały problem polega na tym, że musisz wyjść poza to standardowe zarządzanie.

Jeśli natomiast i tak robisz prosty batch insert, to wariant B z JdbcTemplate jest najczystszy — omijasz Hibernate'a przy zapisie, unikasz problemów z sesją, a TransactionTemplate robi dokładnie to, czego potrzebujesz.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment