Oracle Coherence. Uso de KeyLoader contra Base de Datos.

Oracle Coherence es un grid de datos en memoria. Entre otras cosas, permite distribuir datos entre miembros del cluster, escalar horizontalmente y acercar los datos a las aplicaciones que los consumen.

Una funcionalidad muy útil es el Read-Through Caching: cuando una aplicación pide una clave que no está en memoria, Coherence delega la carga en un componente que sabe leer de un sistema externo, por ejemplo una base de datos. Si encuentra el valor, lo devuelve al cliente y lo deja cacheado para futuras lecturas.

En Coherence ese componente suele implementarse con una de estas interfaces:

Interfaz Uso típico
CacheLoader Solo lectura desde el sistema externo.
CacheStore Lectura y escritura, útil para write-through o write-behind.
BinaryEntryStore Lectura y escritura trabajando con entradas binarias y metadatos de Coherence.

En esta entrada nos centraremos en CacheLoader, suficiente cuando queremos cargar datos desde base de datos pero no escribirlos desde Coherence.

Criterios del ejemplo

El ejemplo sigue estos criterios:

  • El CacheLoader carga entidades por clave.
  • La creación del DataSource se encapsula en una clase separada.
  • Las credenciales llegan por variables de entorno o secretos de Kubernetes.
  • Los objetos cacheados son serializables.
  • Las consultas usan PreparedStatement.
  • Las conexiones, sentencias y resultados se cierran con try-with-resources.
  • loadAll se implementa para permitir cargas por lotes.

Tabla de ejemplo

Supongamos una tabla sencilla:

create table CUSTOMER_CACHE (
  ID         varchar2(64) primary key,
  NAME       varchar2(200) not null,
  SEGMENT    varchar2(30),
  UPDATED_AT timestamp default systimestamp
);

insert into CUSTOMER_CACHE (ID, NAME, SEGMENT)
values ('C001', 'Ada Lovelace', 'PREMIUM');

commit;

Objeto cacheado

Con Java 17 podemos usar un record:

package com.example.coherence;

import java.io.Serializable;

public record Customer(String id, String name, String segment) implements Serializable {
}

Para producción, especialmente si el volumen de datos es alto, conviene configurar POF en lugar de depender de serialización Java estándar.

Crear el DataSource

El ejemplo siguiente usa OracleDataSource y variables de entorno. En producción puedes sustituirlo por UCP, HikariCP, un DataSource gestionado por el contenedor o un secreto inyectado por Kubernetes.

package com.example.coherence;

import oracle.jdbc.pool.OracleDataSource;

import javax.sql.DataSource;
import java.sql.SQLException;

public final class OracleDataSourceFactory {
    private OracleDataSourceFactory() {
    }

    public static DataSource fromEnvironment() {
        try {
            OracleDataSource ds = new OracleDataSource();
            ds.setURL(requiredEnv("DB_URL"));
            ds.setUser(requiredEnv("DB_USER"));
            ds.setPassword(requiredEnv("DB_PASSWORD"));
            return ds;
        } catch (SQLException e) {
            throw new IllegalStateException("No se pudo crear el DataSource de Oracle", e);
        }
    }

    private static String requiredEnv(String name) {
        String value = System.getenv(name);
        if (value == null || value.isBlank()) {
            throw new IllegalStateException("Falta la variable de entorno " + name);
        }
        return value;
    }
}

Variables esperadas:

export DB_URL='jdbc:oracle:thin:@//dbhost.example.com:1521/service_name'
export DB_USER='app_user'
export DB_PASSWORD='secret'

En Kubernetes, estas variables deberían venir de un Secret, no de texto plano en el manifiesto.

Implementar el CacheLoader

package com.example.coherence;

import com.tangosol.net.cache.CacheLoader;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

public final class CustomerCacheLoader implements CacheLoader<String, Customer> {
    private final DataSource dataSource;

    public CustomerCacheLoader() {
        this(OracleDataSourceFactory.fromEnvironment());
    }

    public CustomerCacheLoader(DataSource dataSource) {
        this.dataSource = Objects.requireNonNull(dataSource, "dataSource no puede ser null");
    }

    @Override
    public Customer load(String key) {
        if (key == null || key.isBlank()) {
            return null;
        }

        final String sql = """
            select ID, NAME, SEGMENT
            from CUSTOMER_CACHE
            where ID = ?
            """;

        try (Connection con = dataSource.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {

            ps.setString(1, key);

            try (ResultSet rs = ps.executeQuery()) {
                return rs.next() ? mapRow(rs) : null;
            }
        } catch (SQLException e) {
            throw new IllegalStateException("Error cargando customer con id=" + key, e);
        }
    }

    @Override
    public Map<String, Customer> loadAll(Collection<? extends String> keys) {
        List<String> ids = keys == null
                ? List.of()
                : keys.stream()
                      .filter(Objects::nonNull)
                      .filter(id -> !id.isBlank())
                      .distinct()
                      .toList();

        if (ids.isEmpty()) {
            return Map.of();
        }

        String placeholders = ids.stream()
                .map(id -> "?")
                .collect(Collectors.joining(", "));

        String sql = "select ID, NAME, SEGMENT from CUSTOMER_CACHE where ID in (" + placeholders + ")";

        Map<String, Customer> result = new HashMap<>();

        try (Connection con = dataSource.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {

            for (int i = 0; i < ids.size(); i++) {
                ps.setString(i + 1, ids.get(i));
            }

            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    Customer customer = mapRow(rs);
                    result.put(customer.id(), customer);
                }
            }

            return result;
        } catch (SQLException e) {
            throw new IllegalStateException("Error cargando customers por lote", e);
        }
    }

    private static Customer mapRow(ResultSet rs) throws SQLException {
        return new Customer(
                rs.getString("ID"),
                rs.getString("NAME"),
                rs.getString("SEGMENT")
        );
    }
}

Puntos importantes del ejemplo:

  • No mantiene una conexión JDBC abierta como atributo de clase.
  • Cierra Connection, PreparedStatement y ResultSet con try-with-resources.
  • Usa PreparedStatement.
  • Implementa loadAll.
  • Lanza una excepción clara cuando la base de datos falla.
  • Usa tipos genéricos.

Configurar Coherence para usar el CacheLoader

El CacheLoader se enchufa en un read-write-backing-map-scheme. Ejemplo cache-config.xml:

<?xml version="1.0"?>
<cache-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xmlns="http://xmlns.oracle.com/coherence/coherence-cache-config"
              xsi:schemaLocation="http://xmlns.oracle.com/coherence/coherence-cache-config coherence-cache-config.xsd">

  <caching-scheme-mapping>
    <cache-mapping>
      <cache-name>customers</cache-name>
      <scheme-name>customers-scheme</scheme-name>
    </cache-mapping>
  </caching-scheme-mapping>

  <caching-schemes>
    <distributed-scheme>
      <scheme-name>customers-scheme</scheme-name>
      <service-name>CustomersService</service-name>
      <backing-map-scheme>
        <read-write-backing-map-scheme>
          <internal-cache-scheme>
            <local-scheme/>
          </internal-cache-scheme>
          <cachestore-scheme>
            <class-scheme>
              <class-name>com.example.coherence.CustomerCacheLoader</class-name>
            </class-scheme>
          </cachestore-scheme>
        </read-write-backing-map-scheme>
      </backing-map-scheme>
      <autostart>true</autostart>
    </distributed-scheme>
  </caching-schemes>
</cache-config>

Si el CacheLoader necesita parámetros, puedes usar init-params. En este ejemplo preferimos variables de entorno para no dejar credenciales dentro del XML.

Probar el read-through

Cliente Java mínimo:

package com.example.coherence;

import com.tangosol.net.Coherence;
import com.tangosol.net.NamedMap;
import com.tangosol.net.Session;

public class CustomerClient {
    public static void main(String[] args) {
        Coherence coherence = Coherence.clusterMember();
        coherence.start().join();

        Session session = coherence.getSession();
        NamedMap<String, Customer> customers = session.getMap("customers");

        Customer customer = customers.get("C001");
        System.out.println(customer);

        coherence.close();
    }
}

La primera llamada a customers.get("C001") buscará la clave en memoria. Si no está, Coherence llamará al CustomerCacheLoader, leerá de la tabla CUSTOMER_CACHE y guardará el resultado en la caché.

Uso con Coherence Operator

Cuando ejecutes esto en Kubernetes, empaqueta en la imagen:

/app/conf/cache-config.xml
/app/lib/tu-aplicacion.jar
/app/lib/ojdbc.jar

Y configura el recurso Coherence para cargar ese fichero:

apiVersion: coherence.oracle.com/v1
kind: Coherence
metadata:
  name: coherence-db-cache
  namespace: coherencetest
spec:
  replicas: 3
  image: fra.ocir.io/<ocir-namespace>/coherence-db-cache:1.0.0
  imagePullSecrets:
    - name: ocirsecret

  coherence:
    cacheConfig: cache-config.xml
    storageEnabled: true

  env:
    - name: DB_URL
      valueFrom:
        secretKeyRef:
          name: coherence-db-secret
          key: DB_URL
    - name: DB_USER
      valueFrom:
        secretKeyRef:
          name: coherence-db-secret
          key: DB_USER
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: coherence-db-secret
          key: DB_PASSWORD

El secreto podría crearse así:

kubectl -n coherencetest create secret generic coherence-db-secret \
  --from-literal=DB_URL='jdbc:oracle:thin:@//dbhost.example.com:1521/service_name' \
  --from-literal=DB_USER='app_user' \
  --from-literal=DB_PASSWORD='secret'

¿Y si necesito escritura?

Si Coherence también debe escribir en la base de datos, no uses solo CacheLoader. Implementa CacheStore o usa una implementación JPA si encaja con tu modelo.

Patrones habituales:

  • Read-through: CacheLoader, lectura bajo demanda.
  • Write-through: CacheStore, la escritura se confirma contra base de datos en la misma operación.
  • Write-behind: CacheStore, la escritura se encola y se sincroniza después. Más rápido, pero exige diseñar bien consistencia, reintentos e idempotencia.
  • Refresh-ahead: Coherence refresca entradas antes de que expiren, útil para datos muy leídos.

Recomendaciones para producción

Antes de llevar este patrón a producción, revisa:

  • Usa pool de conexiones. No abras conexiones físicas nuevas para cada operación.
  • Define timeouts JDBC razonables.
  • Implementa loadAll por lotes.
  • Evita SQL dinámico con nombres de tabla o columnas no validados.
  • No devuelvas null para ocultar errores de base de datos.
  • Decide si quieres cachear “no encontrado” con un objeto centinela o dejar que cada get vuelva a consultar.
  • Configura expiración o invalidación si la base de datos puede cambiar por fuera de Coherence.
  • Usa POF u otro formato de serialización controlado.
  • Monitoriza latencia de load, errores JDBC y tamaño de caché.
  • Protege credenciales con secretos de Kubernetes o el mecanismo corporativo equivalente.

Conclusión

CacheLoader es una forma limpia de implementar Read-Through Caching con Oracle Coherence. La clave está en tratarlo como código de infraestructura: conexiones bien gestionadas, SQL parametrizado, errores visibles, loadAll implementado y configuración desacoplada de credenciales.

Enlaces útiles

 

Read More

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *