Om een artikellijst en het bijbehorende aantal consistent te houden, voer je beide opvragingen uit in één REPEATABLE READ READ ONLY-transactie, op dezelfde node-postgres-client. Deze tutorial laat het patroon zien en een controle met twee sessies die je vanaf een Mac kunt uitvoeren.

Waarom kunnen een artikellijst en het bijbehorende aantal van elkaar afwijken?

Stel dat een desktopscript gepubliceerde artikelen in het onderwerp craft ophaalt en vervolgens de overeenkomende artikelen telt. Tussen die opvragingen door wijzigt een andere sessie het onderwerp van een artikel. De geretourneerde lijst kan dan twee artikelen bevatten, terwijl het aantal één aangeeft.

Het standaardisolatieniveau van PostgreSQL, READ COMMITTED, geeft elke gewone SELECT een momentopname aan het begin van die instructie. Opeenvolgende opvragingen kunnen daardoor zelfs binnen één transactie verschillende vastgelegde gegevens zien. PostgreSQL: Transaction Isolation.

Voor deze taak is het gewenste resultaat één samenhangende weergave van de artikelrijen. Een latere bewerking kan bij de volgende uitvoering zichtbaar zijn. PostgreSQLs REPEATABLE READ houdt opeenvolgende opvragingen op de momentopname die wordt vastgelegd door de eerste instructie van de transactie die geen transactiebeheerinstructie is; alleen BEGIN uitvoeren legt die momentopname niet vast. PostgreSQL: Transaction Isolation.

De implementatie heeft twee vereisten: stel het isolatieniveau in voordat je gegevens leest, en voer alle gerelateerde query's uit op dezelfde client. node-postgres vereist expliciet één client voor een transactie en waarschuwt ervoor pool.query() voor de instructies ervan te gebruiken. node-postgres: Transactions.

Maak op je Mac een kleine artikeltestset klaar

Gebruik een actieve lokale PostgreSQL-server, de opdracht psql en een ondersteunde Node.js-installatie met npm. Bij de onderstaande opdrachten wordt ervan uitgegaan dat je bestaande lokale verbinding werkt met de database postgres. Heeft je werkdatabase een andere naam, vervang die dan overal consequent. De installatie van PostgreSQL en het instellen van de verbinding zijn vereisten.

Open Terminal en maak een projectmap aan. Het gedocumenteerde node-postgres-pakket is pg. node-postgres: Welcome.

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

Voer deze eenmalige configuratie uit bij de prompt psql. Hiermee maak je een aparte tabel aan met uitsluitend synthetische artikelrijen. Dankzij de expliciete ID's is de oefening verderop ook gemakkelijk te volgen.

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);

Bestaat de tabel al, stop dan en bekijk de vorige testset in plaats van die automatisch te verwijderen of te vervangen. De voorbeelden gaan uit van precies deze drie beginrijen. Laat psql op \q staan voordat je het Node.js-script uitvoert.

Lees de lijst en het aantal via één opgehaalde client

Sla het volgende volledige programma in de projectmap op als read-articles.mjs. new Pool() gebruikt de gedocumenteerde omgevingsvariabelen voor PostgreSQL-verbindingen, waaronder de hierboven geëxporteerde waarde PGDATABASE. node-postgres: Connecting.

Het programma haalt één client op, start de transactie met de eigenschappen al ingesteld, voert beide opvragingen uit en legt de transactie vast voordat het samengestelde resultaat wordt teruggegeven. PostgreSQL ondersteunt deze eigenschappen rechtstreeks in BEGIN; de transactie eindigt met COMMIT of 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();
}

Voer het programma uit vanuit dezelfde Terminal-sessie:

node read-articles.mjs

Met de ongewijzigde testset bevat het verwachte resultaat de artikelen 1 en 2, en is total gelijk aan de tekenreeks \"2\". Het aantal wordt expliciet als tekst teruggegeven, dus in dit voorbeeld hoef je het niet om te zetten naar een JavaScript-getal. Beide query's gebruiken hetzelfde onderwerp en dezelfde publicatiefilter; isolatie kan niet corrigeren dat de voorwaarden niet overeenkomen.

Opruimen hoort bij een bruikbaar patroon. Bij een mislukte leesbewerking wordt een rollback uitgevoerd, en als die rollback mislukt, wordt de client weggegooid. node-postgres documenteert client.release(true) om een opgehaalde client te vernietigen en pool.end() om de pool van een script af te sluiten. node-postgres: Pool API.

Dit is een zelfstandig script, dus na één uitvoering sluit het de pool af. Behoud in een bestaande applicatie de levenscyclus van de applicatiepool en plaats de transactiefunctie daarin. Elke hulpfunctie die aan deze opvragingen deelneemt, moet de opgehaalde client meekrijgen; een hulpfunctie die ongemerkt pool.query() aanroept, doorbreekt die grens. node-postgres: Transactions.

Controleer de momentopname met twee Terminal-sessies

Een snel script kan klaar zijn voordat een andere sessie iets wijzigt. Voer de volgende oefening in de aangegeven volgorde uit om de relevante timing duidelijk te maken. Dit zijn SQL-stappen per sessie met de al aangemaakte tabel, geen onafhankelijke configuratieprogramma's. De verwachte databankresultaten volgen uit het gedocumenteerde isolatiegedrag; ze worden niet gepresenteerd als een uitgevoerde PostgreSQL-test.

Open twee Terminal-vensters en voer in elk psql -d postgres uit. Herstel in sessie B het onderwerp van de synthetische testset voordat je de lezer start. Wacht tot deze instructie is voltooid:

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

Start in sessie A de lezertransactie en haal de lijst op. Pauzeer nadat de query is voltooid:

-- 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;

De lijst hoort Paper Shapes en Color Studies te bevatten. Schakel nu over naar sessie B en leg een wijziging van het onderwerp vast met deze zelfstandige instructie. Buiten een expliciet transactieblok legt PostgreSQL een geslaagde instructie automatisch vast. PostgreSQL: BEGIN.

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

Ga terug naar sessie A en voltooi de gerelateerde leesbewerking:

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

COMMIT;

Het verwachte aantal is 2, overeenkomstig de eerdere lijst van sessie A. Voer daarna het Node.js-script opnieuw uit; dat hoort alleen Paper Shapes en een totaal van \"1\" terug te geven. Het oude resultaat en het nieuwe resultaat vertegenwoordigen elk een samenhangende weergave.

Herhaal voor een gecontroleerde vergelijking de hele oefening, inclusief het aanvankelijke herstel in sessie B, maar start sessie A met BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;. Houd dezelfde pauze en volgorde van wijzigingen aan. De eerste lijst hoort twee artikelen te bevatten, terwijl het latere aantal 1 is. Zo is de transactie-instelling de enige gewijzigde voorwaarde.

Kies de juiste grens voor gerelateerde leesbewerkingen

Gebruik dit hulpmiddel bij het aanpassen van het voorbeeld. Bepaal eerst welke resultaten met elkaar moeten overeenkomen en kies vervolgens de kleinste databasebewerking die daarin voorziet.

Leestaak Geschikte aanpak Voorwaarde die behouden moet blijven
Eén artikellijst uit één gewone query teruggeven Eén query kan READ COMMITTED gebruiken Geen latere query hoeft dezelfde momentopname te delen.
Een lijst en een afzonderlijk opgevraagd aantal teruggeven Eén REPEATABLE READ READ ONLY-transactie Gebruik één client en overeenkomende filters.
Meerdere batches voor één samenhangend resultaat ophalen Houd de leesbewerkingen binnen dezelfde transactie Een nieuwe transactie levert een nieuwe weergave op.
Leesbewerkingen tussen afzonderlijke databasesessies coördineren Momentopname expliciet exporteren en importeren Afzonderlijke transacties delen niet automatisch een momentopname.

Voor meerdere sessies biedt PostgreSQL pg_export_snapshot() en SET TRANSACTION SNAPSHOT. De exporter moet open blijven totdat de andere transacties de momentopname hebben geïmporteerd. Een importerende transactie moet REPEATABLE READ of SERIALIZABLE gebruiken en de momentopname importeren voordat de eerste query of gegevenswijzigingsinstructie wordt uitgevoerd. Het opdrachtreferentiehandboek beschrijft aanvullende beperkingen voor Serializable-imports. PostgreSQL: Snapshot Synchronization Functions, PostgreSQL: SET TRANSACTION.

Voor deze taak met een artikellijst en bijbehorend aantal voorkom je die extra coördinatie door één client aan te houden. Rond de databaseleesbewerkingen af en sluit de transactie voordat je wacht tot iemand een volgende actie kiest.

Wat lost dit isolatieniveau niet op?

PostgreSQLs REPEATABLE READ voorkomt fantoomlezingen, maar anomalieën bij serialisatie blijven mogelijk. Een stabiele momentopname garandeert dus niet dat gelijktijdige workflows zich gedroegen alsof ze één voor één werden uitgevoerd. Het isolatieniveau bevriest ook geen sequentiewaarden; voor sequenties gelden speciale transactieregels. PostgreSQL: Transaction Isolation.

READ ONLY staat geen wijzigingen aan gewone tabellen toe, behoudens gedocumenteerde uitzonderingen, zoals bepaalde bewerkingen aan tijdelijke tabellen. Het kiest op zichzelf geen stabiele momentopname. Stel beide eigenschappen in voordat je gegevens leest: PostgreSQL staat niet toe dat je het isolatieniveau wijzigt nadat de eerste query of gegevenswijzigingsinstructie van de transactie is uitgevoerd. PostgreSQL: SET TRANSACTION.

Voor rapporten die sterkere Serializable-semantiek vereisen, ondersteunt PostgreSQL SERIALIZABLE READ ONLY DEFERRABLE. Het kan wachten terwijl het de momentopname verkrijgt en wordt daarna uitgevoerd zonder te worden afgebroken wegens een serialisatiefout. Die afweging verschilt van de gewone vereiste voor artikelopvragingen die hier wordt getoond. PostgreSQL: SET TRANSACTION.

Houd voor de artikellijst en het aantal de implementatiegrens expliciet: haal één client op, start de transactie met de vereiste eigenschappen, lees alle gerelateerde resultaten, leg de transactie vast en geef de client vrij. Gebruik dezelfde filters en geef het samengestelde resultaat pas vrij nadat de transactie is voltooid. Zo heeft de volgende desktopactie één samenhangende set artikelgegevens om mee te werken.

Bronnen