En escenarios de procesamiento de datos por lotes o migración, es común necesitar leer grandes volúmenes de información de una base de datos robusta como Oracle, mientras se registra el progreso de la ejecución (por ejemplo, el último ID procesado o una marca de tiempo) en una base de datos ligera y embebida como H2. Esto permite reanudar la tarea desde el punto exacto donde se detuvo en la siguiente ejecución.
Para lograr esta arquitectura en Spring Boot, utilizaremos la librería dynamic-datasource de Baomidou junto con el pool de conexiones Druid.
- Configuración de Dependencias Maven
Primero, incorporamos los controladores JDBC necesarios para Oracle y H2, el starter de Druid y el componente de enrutamiento dinámico de fuentes de datos.
<dependencies>
<!-- Controlador JDBC para Oracle -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>19.8.0.0</version>
</dependency>
<!-- Base de datos embebida H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Pool de conexiones Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.18</version>
</dependency>
<!-- Enrutador dinámico de fuentes de datos -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
</dependencies>
- Definición de las Fuentes de Datos en application.yml
En el archivo de configuración, definimos un datasource principal orientado a la base de datos Oracle y uno secundario para la base de datos H2 que actuará como regitsro de estado. Hemos renombrado las claves para que reflejen mejor su propósito empresarial.
spring:
datasource:
dynamic:
primary: oracle_main
strict: false
datasource:
oracle_main:
url: jdbc:oracle:thin:@//192.168.1.100:1521/XEPDB1
username: admin_user
password: secure_password
driver-class-name: oracle.jdbc.OracleDriver
h2_tracker:
url: jdbc:h2:file:./data/progress_db;AUTO_SERVER=TRUE
username: sa
password:
driver-class-name: org.h2.Driver
- Implementación de la Capa de Servicio
Para dirigir las conusltas a la base de datos correcta, aplicamos la anotación @DS a nivel de clase o método en nuestros servicios. A continuación, se muestra cómo estructurar los servicios para la extracción de datos y el seguiimento del progreso.
Servicio de Extracción de Datos (Oracle)
Este servicio se encarga de interactuar con la base de datos principal para obtener los registros pendientes.
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
@DS("oracle_main")
public class OracleExtractionService {
private final CoreDataRepository coreDataRepository;
public OracleExtractionService(CoreDataRepository coreDataRepository) {
this.coreDataRepository = coreDataRepository;
}
public List<Map<String, Object>> fetchBatchById(String schema, String tableName, Long lastProcessedId, int batchSize) {
return coreDataRepository.selectBatchRecords(schema, tableName, lastProcessedId, batchSize);
}
public List<Map<String, Object>> fetchBatchByTimestamp(String schema, String tableName, String timeColumn, Long startEpoch, Long endEpoch) {
return coreDataRepository.selectRecordsByTimeRange(schema, tableName, timeColumn, startEpoch, endEpoch);
}
}
Servicio de Seguimiento de Progreso (H2)
Este servicio gestiona la lectura y actualización de los puntos de control (checkpoints) en la base de datos embebida.
import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
@Service
@DS("h2_tracker")
public class ProgressTrackerService {
private final CheckpointRepository checkpointRepository;
public ProgressTrackerService(CheckpointRepository checkpointRepository) {
this.checkpointRepository = checkpointRepository;
}
public Long getLastProcessedId(String jobName) {
CheckpointEntity checkpoint = checkpointRepository.findByJobName(jobName);
return (checkpoint != null) ? checkpoint.getLastId() : 0L;
}
public void saveCurrentProgress(String jobName, Long currentId, Long currentTime) {
checkpointRepository.upsertCheckpoint(jobName, currentId, currentTime);
}
public void initializeJob(String jobName) {
if (!checkpointRepository.existsByJobName(jobName)) {
checkpointRepository.insertNewJob(jobName, System.currentTimeMillis());
}
}
}