En corto

1
GRANT CHANGE NOTIFICATION TO app_user;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import oracledb

def on_change(message):
    for query in message.queries:
        for table in query.tables:
            for row in table.rows:
                print(table.name, row.operation, row.rowid)

conn = oracledb.connect(user="app_user", password=pw,
                        dsn="db.example:1521/orclpdb1", events=True)

sub = conn.subscribe(callback=on_change,
                     operations=oracledb.OPCODE_INSERT | oracledb.OPCODE_UPDATE | oracledb.OPCODE_DELETE,
                     qos=oracledb.SUBSCR_QOS_QUERY | oracledb.SUBSCR_QOS_ROWIDS)
sub.registerquery("select id, status, updated_at from orders")   # columnas explícitas, nunca SELECT *

input("Ctrl+C para salir\n")

Cada COMMIT que cambie el resultado de esa consulta dispara on_change con los rowid afectados. Sin polling, sin triggers, sin GoldenGate.

Por qué

Cuando otro sistema necesita reaccionar a cambios en Oracle (invalidar una caché, replicar hacia otra base, disparar un proceso), lo habitual es preguntar cada N segundos con WHERE updated_at > :last. Funciona hasta que no: filas sin columna de fecha, borrados que no dejan rastro, carga inútil sobre la base.

Continuous Query Notification invierte el flujo: registras una consulta y Oracle te llama cuando cambia. En modo query (SUBSCR_QOS_QUERY) avisa solo si cambia el resultado de esa consulta; en modo object (sin ese flag) avisa ante cualquier cambio en las tablas implicadas. SUBSCR_QOS_ROWIDS incluye los rowid en la notificación, con lo que el callback puede leer exactamente esas filas.

El caso: replicar Oracle a PostgreSQL

oracle_fdw deja consultar tablas de Oracle desde PostgreSQL como si fueran locales, y es tentador pensar que un trigger sobre la foreign table se disparará cuando cambien los datos en Oracle. No: los triggers de una foreign table solo saltan con escrituras hechas desde PostgreSQL a través de ella. El autor de oracle_fdw lo dice tal cual: no hay forma automática de mantener sincronizada una tabla de PostgreSQL con una de Oracle desde el lado PostgreSQL; eso tiene que dispararlo Oracle.

Con CQN el pequeño servicio en Python es la pieza que falta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def on_change(message):
    rowids = [row.rowid for q in message.queries for t in q.tables for row in t.rows]
    with conn.cursor() as cur:
        cur.execute("select id, status, updated_at from orders where rowid in (%s)"
                    % ",".join(f"'{r}'" for r in rowids))
        for id_, status, updated_at in cur:
            pg.execute("insert into orders (id, status, updated_at) values (%s, %s, %s) "
                       "on conflict (id) do update set status = excluded.status, updated_at = excluded.updated_at",
                       (id_, status, updated_at))
    pg.commit()

Los borrados llegan con row.operation & oracledb.OPCODE_DELETE y un rowid que ya no existe: borra en PostgreSQL por la clave que guardaste asociada a ese rowid, o marca la fila.

Requisitos

  • Permiso: GRANT CHANGE NOTIFICATION TO app_user, además de SELECT sobre las tablas.
  • events=True en la conexión que se suscribe; sin eso subscribe() falla.
  • Red de vuelta. Por defecto es la base la que abre una conexión hacia tu aplicación para entregar las notificaciones: la máquina del cliente necesita una IP alcanzable desde el servidor Oracle y un puerto abierto (subscribe(..., ip_address=…, port=…) fija cuáles). Detrás de NAT, en un contenedor o con firewall entre medias, las notificaciones simplemente no llegan y nadie avisa.
  • client_initiated=True elimina ese problema: la conexión la abre tu app. Requiere modo Thick (oracledb.init_oracle_client() con Instant Client 19.4 o superior) y base 19.4 o superior. En modo Thin, el por defecto de python-oracledb, CQN funciona pero solo en el modo clásico.
  • job_queue_processes mayor que cero en la base: las notificaciones se despachan por el job queue.

Trampas

  • ORA-29977: Unsupported column type for query registration in guaranteed mode: en modo query hay tipos de columna que Oracle no admite (LOB, entre otros), y SELECT * los arrastra. Lista las columnas que necesitas o quita SUBSCR_QOS_QUERY y trabaja en modo object.
  • Las notificaciones no son durables: si tu servicio está caído durante un cambio, no lo recibe. Al arrancar, haz una reconciliación (por updated_at, o completa) y después suscríbete. timeout=0 en subscribe mantiene la suscripción hasta que la conexión muera.
  • Una suscripción por conexión viva: la conexión con events=True tiene que seguir abierta; ponle keepalives y reconecta con re-suscripción si se cae.
  • Mucho volumen (miles de commits por segundo) no es territorio de CQN: ahí entra un CDC por redo log (GoldenGate, o Debezium con su conector de Oracle vía LogMiner).
  • SELECT con funciones no deterministas o sobre vistas complejas puede no registrarse en modo query; simplifica la consulta o cae a modo object.

Referencia: Continuous Query Notification en la documentación de python-oracledb y la discusión en oracle_fdw.