Da transakcija node-postgres ostane povezana, pridobite enega odjemalca z pool.connect(), izvedite vse stavke prek tega odjemalca in ga sprostite po potrditvi ali povrnitvi transakcije. Tega odjemalca posredujte tudi pomožnim funkcijam. Uradni vodič Transactions zahteva ta vzorec.

Ta vadnica je namenjena razvijalcu, ki v Macovem terminalu želi doseči, da več sprememb vrstic s članki uspe ali spodleti skupaj. Vključuje celoten program z izmišljenimi vrsticami člankov, namenski preizkus povrnitve transakcije in diagnostični postopek za poizvedbe, skrite v pomožnih funkcijah.

Zakaj pool.query() poruši obseg transakcije?

Skupina povezav upravlja povezave; ne zagotavlja, da vsaka poizvedba pripada eni transakciji. Vsak klic pool.query() interno pridobi razpoložljivega odjemalca. Zato ni zagotovljeno, da bodo zaporedni klici uporabili isto povezavo. API node-postgres Pool izrecno opozarja, da te metode ne uporabljate za transakcije.

Če na primer prek skupine povezav pošljete BEGIN, nato pa prek skupine pošljete še vstavljanje, to vstavljanje ne bo zanesljivo del transakcije, ki ste jo začeli. PostgreSQL stavke zunaj izrecnega transakcijskega bloka običajno izvede v njihovih lastnih transakcijah. Poznejša povrnitev na drugi povezavi ne more razveljaviti teh neodvisno potrjenih sprememb. Glejte PostgreSQL-ovo dokumentacijo za BEGIN.

Ključna meja je objekt odjemalca, ki ste ga prevzeli iz skupine povezav. Ime pomožne funkcije, modul, v katerem je, in dejstvo, da jo kličoča funkcija čaka z await, sami po sebi ne določajo, ali je funkcija del transakcije. Pomožna funkcija sodeluje v transakciji, ko njene poizvedbe uporabljajo odjemalca, ki je transakcijo začel. To neposredno izhaja iz zahteve po istem odjemalcu v vodiču Transactions.

Zaženite celoten primer potrditve in povrnitve transakcije na svojem Macu

Potrebujete Node.js z npm, delujoč strežnik PostgreSQL in obstoječo lokalno razvojno zbirko podatkov. Predpostavljamo, da se ta zbirka imenuje article_demo in da vaše običajne lokalne nastavitve povezave že delujejo. Namestitev zbirke podatkov ni del te naloge o transakcijah.

V terminalu ustvarite nov delovni imenik in namestite gonilnik:

mkdir pg-transaction-demo
cd pg-transaction-demo
npm init -y
npm install pg

Naslednji celoten program shranite kot transaction-demo.mjs. Njegova začasna tabela vsebuje le izmišljene ID-je in naslove člankov. Začasne tabele PostgreSQL običajno ostanejo po potrditvi transakcije in se odstranijo, ko se seja konča; te lastnosti programu omogočajo, da po povrnitvi drugega sklopa pregleda uspešno zaključenega. Glejte CREATE TABLE.

Spodnja pomožna funkcija za transakcije sledi dokumentiranemu zaporedju BEGIN, COMMIT in ROLLBACK. Izrecno obravnava tudi primer, ko spodleti še povrnitev transakcije, tako da ohrani obe napaki, namesto da bi prvotno napako zamenjala z drugo. Vrednosti poizvedb uporabljajo API za besedilo in vrednosti, opisan v razdelku Queries.

import pg from 'pg';
import assert from 'node:assert/strict';

const { Pool } = pg;

async function inTransaction(client, work) {
  await client.query('BEGIN ISOLATION LEVEL READ COMMITTED');

  try {
    const result = await work(client);
    await client.query('COMMIT');
    return result;
  } catch (error) {
    try {
      await client.query('ROLLBACK');
    } catch (rollbackError) {
      throw new AggregateError(
        [error, rollbackError],
        'Transaction failed; rollback also failed'
      );
    }
    throw error;
  }
}

async function insertArticle(client, article) {
  const result = await client.query(
    `INSERT INTO pg_temp.transaction_demo_articles (id, title)
     VALUES ($1, $2)
     RETURNING id, title`,
    [article.id, article.title]
  );
  return result.rows[0];
}

async function insertBatch(client, articles) {
  const inserted = [];
  for (const article of articles) {
    inserted.push(await insertArticle(client, article));
  }
  return inserted;
}

async function main() {
  const pool = new Pool({ connectionTimeoutMillis: 5000 });
  let client;
  let discardClient = false;

  pool.on('error', (error) => {
    console.error('Idle client error:', error);
    process.exitCode = 1;
  });

  try {
    client = await pool.connect();

    await client.query(`
      CREATE TEMP TABLE transaction_demo_articles (
        id text PRIMARY KEY,
        title text NOT NULL
      ) ON COMMIT PRESERVE ROWS
    `);

    const saved = await inTransaction(client, (tx) =>
      insertBatch(tx, [
        { id: 'article-001', title: 'A guide to paper shapes' },
        { id: 'article-002', title: 'Choosing a notebook layout' }
      ])
    );
    assert.equal(saved.length, 2);

    await assert.rejects(
      () => inTransaction(client, (tx) =>
        insertBatch(tx, [
          { id: 'article-003', title: 'Arranging a reading desk' },
          { id: 'article-001', title: 'Deliberate duplicate ID' }
        ])
      ),
      (error) => error.code === '23505'
    );

    const remaining = await client.query(`
      SELECT id, title
      FROM pg_temp.transaction_demo_articles
      ORDER BY id
    `);

    assert.deepEqual(
      remaining.rows.map((row) => row.id),
      ['article-001', 'article-002']
    );

    console.log('Commit and rollback checks passed.');
    console.table(remaining.rows);
  } catch (error) {
    discardClient = true;
    throw error;
  } finally {
    if (client) client.release(discardClient);
    await pool.end();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Zaženite ga iz istega imenika:

PGDATABASE=article_demo node transaction-demo.mjs

Skupina povezav bere okoljske spremenljivke za povezavo s PostgreSQL. Če vaša lokalna namestitev zahteva drugačne nastavitve povezave, poleg PGDATABASE podajte ustrezna PGHOST in PGPORT. Uradni vodič Connecting opisuje te spremenljivke in njihove privzete vrednosti.

Ob uspešnem zagonu pričakujte izpis Commit and rollback checks passed. in nato dve prvotni vrstici člankov. Drugi sklop vstavi article-003, nato pa poskusi znova uporabiti article-001. Zaradi podvojenega vnosa nastane SQLSTATE 23505, ki je v razdelku Serialization Failure Handling dokumentiran kot unique_violation. Povrnitev sklopa odstrani tudi njegovo predhodno vstavljanje; trditev preveri, da article-003 manjka.

Uspešna pot, pot ob napaki pri delu in pot ob napaki pri povrnitvi transakcije v pomožni funkciji so bile preverjene z deterministično posnemano stranko. Primer z zbirko podatkov tukaj ni bil izveden, ker v okolju nista bila na voljo paket pg in testna povezava s PostgreSQL; zgornji izpis prikazuje pričakovani rezultat.

Kako naj si pomožne funkcije delijo odjemalca transakcije?

Odjemalca izrecno posredujte skozi celotno klicno verigo. V programu main() pridobi odjemalca, inTransaction() isti objekt posreduje povratni funkciji, insertBatch() pa ga posreduje naprej funkciji insertArticle(). Nobena od pomožnih funkcij za vstavljanje ne prevzame povezave iz skupine povezav.

Pri tej implementaciji jasno določite odgovornosti:

Funkcija Odgovornost
main() Pridobitev odjemalca, priprava predstavitvenih podatkov, sprostitev odjemalca in zaprtje skupine povezav skripta.
inTransaction() Začetek in zaključek ene transakcije na posredovanem odjemalcu.
insertBatch() Čakanje na vsako operacijo članka in posredovanje danega odjemalca.
insertArticle() Izvedba vstavljanja prek danega odjemalca.

Pogodba za pomožno funkcijo je namenoma ozka: inTransaction() pokličite le, ko odjemalec nima aktivne transakcije, napake pa naj se širijo iz njene povratne funkcije. Klic BEGIN znotraj obstoječe transakcije PostgreSQL ne ustvari neodvisne ugnezdene transakcije; PostgreSQL kot mehanizem za ugnezdeno delo dokumentira shranjevalne točke. Glejte BEGIN.

Preden vrnete nadzor iz povratne funkcije, počakajte na vsako operacijo, ki spada v transakcijo. V tem primeru zaporedna zanka jasno določa točko, ko so vse operacije končane. Če asinhrona vstavljanja začnete, ne da bi vrnili njihove obljube ali počakali nanje, se lahko povratna funkcija konča, še preden prejme rezultate vseh vstavljanj.

Kaj se mora zgoditi, če poizvedba ali povrnitev transakcije spodleti?

Običajna napaka poizvedbe doseže blok catch v pomožni funkciji za transakcije, ki poskusi povrniti transakcijo na istem odjemalcu in znova sproži prvotno napako. Namensko podvajanje je sprejeto šele, ko je povrnitev transakcije končana. Če povrnitev spodleti, pomožna funkcija sproži AggregateError; preverjanje napake zaradi podvojenega vnosa te napake ne sprejme, zato se izvajanje nadaljuje do zunanjega obravnavalnika napak.

V primeru skript ob vsaki nepričakovani napaki previdnostno uniči odjemalca z uporabo client.release(true). Ob običajnem poteku odjemalca vrne z client.release(false). Argument za uničenje je dokumentiran v API-ju Pool.

Pridobljenega odjemalca vedno sprostite. Sicer bodo naslednji klicatelji morda čakali na povezavo, ki nikoli ne bo na voljo. Ko se ta samostojni skript konča, zaprite skupino povezav; v aplikaciji pa zapiranje skupine povezav spada v postopek zaustavitve aplikacije. Vodič Pooling pojasnjuje zahteve za sprostitev in opisuje, kako pool.end() počaka, da se prevzeti odjemalci vrnejo.

Ali en odjemalec zagotavlja stabilen posnetek?

Neprekinjenost odjemalca in raven izolacije odgovarjata na različni vprašanji. Odjemalec določa, kateri transakciji bodo pripadale vaše izjave. Raven izolacije določa, kaj lahko te izjave vidijo med izvajanjem drugih transakcij. PostgreSQL-ova dokumentacija o izolaciji transakcij določa te omejitve:

Raven izolacije Ustrezna omejitev
READ COMMITTED Privzeta vrednost. Zaporedna običajna branja lahko vidijo različne sočasno potrjene podatke. Vsak stavek dobi nov posnetek.
REPEATABLE READ Uporablja posnetek, ustvarjen ob prvem stavku, ki ni namenjen nadzoru transakcije. PostgreSQL preprečuje fantomska branja, vendar so anomalije serializacije še vedno mogoče.
SERIALIZABLE Potrjene serializabilne transakcije se obnašajo skladno z nekim zaporednim izvajanjem; transakcija lahko spodleti in jo je treba ponoviti.
READ UNCOMMITTED V PostgreSQL se obnaša kot READ COMMITTED.

Branja lahko vidijo tudi spremembe, ki jih je prej izvedla njihova lastna transakcija. Zato to, da vidite svoje vstavljanje, še ne pomeni, da imajo vsa poznejša branja nespremenljiv pogled na spremembe drugih transakcij. Glejte Transaction Isolation.

Primer v začetnem stavku izrecno izbere READ COMMITTED. Če želite uporabiti drugo raven, pred zagonom transakcije spremenite ta stavek. PostgreSQL dovoljuje določanje lastnosti transakcije v BEGIN; če namesto tega uporabite SET TRANSACTION, morate raven izolacije nastaviti pred prvo poizvedbo ali stavkom za spreminjanje podatkov. Glejte BEGIN in SET TRANSACTION.

Če izberete REPEATABLE READ ali SERIALIZABLE, bodite pripravljeni na SQLSTATE 40001. Ponovite celotno transakcijo, vključno z logiko, ki izbira stavke in vrednosti, namesto da bi znova izvedli samo poizvedbo, ki je spodletela. Primer takšne napake namenoma posreduje naprej in ne izvaja ponovnih poskusov. PostgreSQL-ov Serialization Failure Handling pojasnjuje to zahtevo in opozarja, da lahko ponovni poskusi spet spodletijo.

Kako najdete poizvedbo, ki se je izvajala zunaj transakcije?

Sledite enemu predvidenemu sklopu od pridobitve povezave do končne potrditve ali povrnitve transakcije. Za pregled kode uporabite naslednje zaporedje:

  1. Poiščite klic pool.connect() in določite, katerega odjemalca vrne.
  2. Sledite vsaki pomožni funkciji, ki je poklicana med BEGIN in zaključkom transakcije. Preverite, ali vsaka prejme in uporablja tega odjemalca.
  3. Preglejte skupne ovojnice za poizvedbe in poiščite skrite klice pool.query() ali dodatne prevzeme povezav.
  4. Preverite, ali se na vsako operacijo počaka in ali pomožne funkcije posredujejo napake naprej.
  5. Preizkus podvojene vrstice z izmišljenimi podatki prilagodite svojemu sklopu in preverite, ali predhodno vstavljanje izgine, ko poznejši stavek spodleti.

Uspešen preizkus povrnitve transakcije je koristen dokaz za preizkušeno pot, vendar ne more potrditi, da vsaka veja aplikacije uporablja pravega odjemalca. Preglejte tudi pogojne pomožne funkcije. Če poizvedbe ostajajo na istem odjemalcu, vendar se ponovljena branja še vedno razlikujejo, pred spremembo ravnanja s povezavami primerjajte to vedenje z izbrano ravnjo izolacije.

Pridobivanje in sproščanje odjemalca naj bosta jasno vidna, odjemalca izrecno posredujte, zaključek transakcije pa pogojujte s tem, da se končajo vse predvidene operacije. Tako dobite konkretno strukturo za pregled in preizkus povrnitve, ki ga lahko ponavljate, ko se potek dela s članki širi.

Viri