Para mantener coherentes una lista de artículos y su recuento, ejecuta ambas lecturas en una transacción REPEATABLE READ READ ONLY con el mismo cliente de node-postgres. Este tutorial muestra el patrón y una comprobación con dos sesiones que puedes ejecutar desde un Mac.

¿Por qué pueden no coincidir una lista de artículos y su recuento?

Supongamos que un script de escritorio obtiene artículos publicados en el tema craft y luego cuenta los artículos que coinciden. Entre esas lecturas, otra sesión cambia el tema de un artículo. La lista devuelta puede contener dos artículos mientras que el recuento indica uno.

El nivel de aislamiento predeterminado de PostgreSQL, READ COMMITTED, asigna una instantánea al inicio de cada SELECT ordinaria. Por lo tanto, las lecturas sucesivas pueden ver datos confirmados distintos, incluso dentro de una misma transacción. PostgreSQL: Aislamiento de transacciones.

Para esta tarea, el resultado deseado es una vista coherente de las filas de artículos. Una edición posterior puede aparecer en la siguiente ejecución. REPEATABLE READ de PostgreSQL mantiene las lecturas sucesivas en la instantánea establecida por la primera instrucción de la transacción que no sea de control de transacciones; ejecutar simplemente BEGIN no establece esa instantánea. PostgreSQL: Aislamiento de transacciones.

La implementación tiene dos requisitos: seleccionar el nivel de aislamiento antes de leer y mantener todas las consultas relacionadas en el mismo cliente. node-postgres exige explícitamente un único cliente para una transacción y advierte que no se debe usar pool.query() para sus instrucciones. node-postgres: Transacciones.

Prepara un pequeño conjunto de datos de artículos en tu Mac

Usa un servidor PostgreSQL local en ejecución, el comando psql y una instalación compatible de Node.js con npm. Los comandos siguientes presuponen que tu conexión local existente funciona con la base de datos postgres. Si tu base de datos de trabajo tiene otro nombre, sustitúyelo de forma coherente. La instalación de PostgreSQL y la configuración de la conexión son requisitos previos.

Abre Terminal y crea una carpeta para el proyecto. El paquete documentado de node-postgres es pg. node-postgres: Introducción.

mkdir article-snapshot-demo
cd article-snapshot-demo
npm init -y
npm install pg
export PGDATABASE=postgres
psql -d postgres

En el indicador de psql, ejecuta esta configuración una vez. Crea una tabla dedicada que contiene solo filas sintéticas de artículos. Los identificadores explícitos también facilitan seguir el ejercicio posterior.

CREATE TABLE public.article_snapshot_demo (
    id integer PRIMARY KEY,
    title text NOT NULL,
    topic text NOT NULL,
    published boolean NOT NULL
);

INSERT INTO public.article_snapshot_demo
    (id, title, topic, published)
VALUES
    (1, 'Paper Shapes', 'craft', true),
    (2, 'Color Studies', 'craft', true),
    (3, 'Desk Layouts', 'design', true);

Si la tabla ya existe, detente e inspecciona el conjunto de datos anterior en lugar de eliminarlo o reemplazarlo automáticamente. Los ejemplos presuponen exactamente estas tres filas iniciales. Deja psql con \q antes de ejecutar el script de Node.js.

Lee la lista y el recuento con un único cliente adquirido

Guarda el siguiente programa completo como read-articles.mjs en la carpeta del proyecto. new Pool() usa las variables de entorno de conexión de PostgreSQL documentadas, incluido el valor PGDATABASE exportado anteriormente. node-postgres: Conexión.

El programa adquiere un cliente, inicia la transacción con sus características ya especificadas, realiza ambas lecturas y confirma la transacción antes de devolver el resultado ensamblado. PostgreSQL admite estas características directamente en BEGIN; la transacción termina con COMMIT o ROLLBACK. PostgreSQL: BEGIN.

import pg from 'pg';
const { Pool } = pg;

const pool = new Pool({ connectionTimeoutMillis: 5000 });
pool.on('error', (error) => {
  console.error('Idle database client error:', error.message);
  process.exitCode = 1;
});

async function readArticles(topic) {
  const client = await pool.connect();
  let began = false;
  let discardClient = false;

  try {
    await client.query(
      'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'
    );
    began = true;

    const articles = await client.query(
      `SELECT id, title, topic
       FROM public.article_snapshot_demo
       WHERE topic = $1 AND published = true
       ORDER BY id`,
      [topic]
    );

    const count = await client.query(
      `SELECT count(*)::text AS total
       FROM public.article_snapshot_demo
       WHERE topic = $1 AND published = true`,
      [topic]
    );

    await client.query('COMMIT');
    began = false;

    return {
      topic,
      articles: articles.rows,
      total: count.rows[0].total
    };
  } catch (error) {
    if (began) {
      try {
        await client.query('ROLLBACK');
      } catch (rollbackError) {
        discardClient = true;
        throw new AggregateError(
          [error, rollbackError],
          'Article read failed; rollback also failed'
        );
      }
    } else {
      discardClient = true;
    }
    throw error;
  } finally {
    client.release(discardClient);
  }
}

try {
  const result = await readArticles('craft');
  console.log(JSON.stringify(result, null, 2));
} catch (error) {
  console.error(error);
  process.exitCode = 1;
} finally {
  await pool.end();
}

Ejecuta el programa desde la misma sesión de Terminal:

node read-articles.mjs

Con el conjunto de datos sin cambios, el resultado esperado contiene los artículos 1 y 2, y total es igual a la cadena \"2\". El recuento se devuelve explícitamente como texto, así que este ejemplo no requiere convertirlo en un número de JavaScript. Ambas consultas usan el mismo filtro de tema y publicación; el aislamiento no puede corregir predicados que no coincidan.

La limpieza forma parte del patrón utilizable. Una lectura fallida provoca una reversión, y si esta falla, el cliente se descarta. node-postgres documenta client.release(true) para destruir un cliente adquirido y pool.end() para cerrar el grupo de conexiones de un script. node-postgres: API de Pool.

Este es un script independiente, por lo que cierra su grupo de conexiones después de una ejecución. En una aplicación existente, mantén el ciclo de vida del grupo de conexiones de la aplicación y sitúa dentro de él la función de transacción. Todas las funciones auxiliares que participen en estas lecturas deben recibir el client adquirido; una función auxiliar que llame a pool.query() sin indicarlo rompe ese límite. node-postgres: Transacciones.

Comprueba la instantánea con dos sesiones de Terminal

Un script rápido puede terminar antes de que otra sesión cambie algo. Usa el siguiente ejercicio, ordenado deliberadamente, para hacer explícita la sincronización pertinente. Estos son pasos de SQL específicos de cada sesión que usan la tabla ya creada, no programas de configuración independientes. Los resultados esperados de la base de datos se basan en el comportamiento de aislamiento documentado; no se presentan como una prueba de PostgreSQL ejecutada.

Abre dos ventanas de Terminal y ejecuta psql -d postgres en cada una. Antes de iniciar la lectura, restaura en la sesión B el tema del conjunto de datos sintético. Espera a que termine esta instrucción:

-- Session B: restore the fixture before beginning the exercise.
UPDATE public.article_snapshot_demo
SET topic = 'craft'
WHERE id = 2;

En la sesión A, inicia la transacción de lectura y obtén la lista. Haz una pausa cuando termine la consulta:

-- Session A: run these statements, then pause.
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;

SELECT id, title
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true
ORDER BY id;

La lista debería contener Paper Shapes y Color Studies. Ahora cambia a la sesión B y confirma un cambio de tema con esta instrucción independiente. Fuera de un bloque de transacción explícito, PostgreSQL confirma automáticamente las instrucciones que se ejecutan correctamente. PostgreSQL: BEGIN.

-- Session B: run while session A remains open.
UPDATE public.article_snapshot_demo
SET topic = 'design'
WHERE id = 2;

Vuelve a la sesión A y completa la lectura relacionada:

-- Session A: finish the original reader transaction.
SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true;

COMMIT;

El recuento esperado es 2, que coincide con la lista anterior de la sesión A. Después, una nueva ejecución del script de Node.js debería devolver solo Paper Shapes y un total de \"1\". Tanto el resultado anterior como el nuevo representan una vista coherente.

Para hacer una comparación controlada, repite todo el ejercicio, incluida la restauración inicial en la sesión B, pero inicia la sesión A con BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;. Mantén la misma pausa y el mismo orden de actualización. La primera lista esperada contiene dos artículos, mientras que el recuento posterior es 1. Esta comparación mantiene constante todo salvo la configuración de la transacción.

Elige el límite adecuado para las lecturas relacionadas

Usa esta guía de decisión al adaptar el ejemplo. Primero decide qué resultados deben coincidir y luego elige la operación de base de datos más pequeña que los proporcione.

Tarea del lector Enfoque adecuado Condición que se debe mantener
Devolver una lista de artículos con una sola consulta ordinaria Una consulta puede usar READ COMMITTED Ninguna consulta posterior necesita compartir su instantánea.
Devolver una lista y un recuento consultado por separado Una transacción REPEATABLE READ READ ONLY Usa un único cliente y filtros coincidentes.
Obtener varios lotes para un único resultado coherente Mantén las lecturas dentro de la misma transacción Una nueva transacción establece una vista nueva.
Coordinar lecturas en sesiones de base de datos separadas Exportación e importación explícitas de instantáneas Las transacciones independientes no comparten una instantánea automáticamente.

Para varias sesiones, PostgreSQL proporciona pg_export_snapshot() y SET TRANSACTION SNAPSHOT. La transacción que exporta la instantánea debe permanecer abierta hasta que las demás transacciones la importen. Una transacción que importe una instantánea debe usar REPEATABLE READ o SERIALIZABLE e importarla antes de su primera consulta o instrucción de modificación de datos. La referencia de comandos documenta restricciones adicionales para la importación serializable. PostgreSQL: Funciones de sincronización de instantáneas, PostgreSQL: SET TRANSACTION.

Para esta tarea de lista y recuento en un equipo de escritorio, conservar un único cliente evita esa coordinación adicional. Termina las lecturas de la base de datos y cierra la transacción antes de esperar a que una persona elija otra acción.

¿Qué aspectos quedan sin resolver con este nivel de aislamiento?

REPEATABLE READ de PostgreSQL evita las lecturas fantasma, pero aún pueden producirse anomalías de serialización. Por lo tanto, una instantánea estable no garantiza que los flujos de trabajo concurrentes se hayan comportado como si se hubieran ejecutado uno a la vez. Tampoco congela los valores de las secuencias, cuyo comportamiento transaccional es especial. PostgreSQL: Aislamiento de transacciones.

READ ONLY impide los cambios en tablas ordinarias, con excepciones documentadas, como ciertas operaciones con tablas temporales. Por sí solo, no elige una instantánea estable. Establece ambas características antes de leer: PostgreSQL no permitirá cambiar el nivel de aislamiento después de la primera consulta o instrucción de modificación de datos de la transacción. PostgreSQL: SET TRANSACTION.

Cuando un informe requiere semántica serializable más estricta, PostgreSQL admite SERIALIZABLE READ ONLY DEFERRABLE. Puede esperar mientras obtiene su instantánea y luego se ejecuta sin cancelaciones por fallos de serialización. Esa compensación es distinta del requisito habitual de lectura de artículos que se muestra aquí. PostgreSQL: SET TRANSACTION.

Para la lista y el recuento de artículos, mantén explícito el límite de implementación: adquiere un cliente, inicia la transacción con las características requeridas, lee todos los resultados relacionados, confirma la transacción y libera el cliente. Reutiliza los mismos filtros y publica el resultado ensamblado solo después de que termine la transacción. Así, la siguiente acción en el escritorio dispone de un conjunto coherente de datos de artículos.

Fuentes