Pour que la liste d’articles et son décompte restent cohérents, exécutez les deux lectures dans une même transaction REPEATABLE READ READ ONLY, avec le même client node-postgres. Ce tutoriel présente cette méthode et une vérification en deux sessions que vous pouvez effectuer depuis un Mac.
Pourquoi la liste d’articles peut-elle ne pas correspondre à son décompte ?
Supposons qu’un script pour ordinateur de bureau récupère les articles publiés dans le sujet craft, puis compte les articles correspondants. Entre ces deux lectures, une autre session modifie le sujet d’un article. La liste renvoyée peut contenir deux articles alors que le décompte en indique un.
Le niveau d’isolation par défaut de PostgreSQL, READ COMMITTED, fournit à chaque instruction SELECT ordinaire un instantané au début de son exécution. Des lectures successives peuvent donc voir des données validées différentes, même au sein d’une même transaction. PostgreSQL : isolation des transactions.
Pour cette tâche, le résultat souhaité est une vue cohérente des lignes d’articles. Une modification ultérieure pourra apparaître lors de la prochaine exécution. REPEATABLE READ de PostgreSQL conserve le même instantané pour les lectures successives, établi par la première instruction de la transaction qui ne contrôle pas la transaction ; la simple exécution de BEGIN ne crée pas cet instantané. PostgreSQL : isolation des transactions.
L’implémentation doit respecter deux exigences : définir le niveau d’isolation avant toute lecture et exécuter toutes les requêtes associées avec le même client. node-postgres exige explicitement un client unique pour une transaction et déconseille d’utiliser pool.query() pour ses instructions. node-postgres : transactions.
Préparer un petit jeu de données d’articles sur votre Mac
Utilisez un serveur PostgreSQL local en cours d’exécution, la commande psql et une installation prise en charge de Node.js avec npm. Les commandes ci-dessous supposent que votre connexion locale existante fonctionne avec la base de données postgres. Si votre base de travail porte un autre nom, remplacez-le partout de façon cohérente. L’installation de PostgreSQL et la configuration de la connexion sont des prérequis.
Ouvrez Terminal et créez un dossier de projet. Le paquet node-postgres documenté est pg. node-postgres : bienvenue.
mkdir article-snapshot-demo
cd article-snapshot-demo
npm init -y
npm install pg
export PGDATABASE=postgres
psql -d postgres
À l’invite psql, exécutez cette configuration une seule fois. Elle crée une table dédiée contenant uniquement des lignes d’articles synthétiques. Les identifiants explicites facilitent également le suivi de l’exercice.
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 table existe déjà, arrêtez-vous et examinez le jeu de données précédent au lieu de le supprimer ou de le remplacer automatiquement. Les exemples supposent exactement ces trois lignes de départ. Dans psql, laissez \q avant d’exécuter le script Node.js.
Lire la liste et le décompte avec un seul client réservé
Enregistrez le programme complet suivant sous le nom read-articles.mjs dans le dossier du projet. new Pool() utilise les variables d’environnement de connexion PostgreSQL documentées, dont la valeur PGDATABASE exportée ci-dessus. node-postgres : connexion.
Le programme réserve un client, démarre la transaction en précisant ses caractéristiques dès le début, effectue les deux lectures, puis valide la transaction avant de renvoyer le résultat assemblé. PostgreSQL accepte directement ces caractéristiques dans BEGIN ; la transaction se termine avec COMMIT ou 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();
}
Exécutez le programme depuis la même session Terminal :
node read-articles.mjs
Avec le jeu de données inchangé, le résultat attendu contient les articles 1 et 2, et total vaut la chaîne "2". Le décompte est explicitement renvoyé sous forme de texte ; cet exemple ne nécessite donc pas de conversion en nombre JavaScript. Les deux requêtes utilisent le même filtre de sujet et de publication ; l’isolation ne peut pas corriger des prédicats différents.
La gestion de la fermeture fait partie de cette méthode utilisable. Une lecture échouée déclenche une annulation, et si cette annulation échoue, le client est abandonné. node-postgres documente client.release(true) pour détruire un client réservé et pool.end() pour fermer le pool d’un script. node-postgres : API du pool.
Il s’agit d’un script autonome : il ferme donc son pool après une exécution. Dans une application existante, conservez le cycle de vie du pool de l’application et intégrez-y la fonction de transaction. Chaque fonction auxiliaire participant à ces lectures doit recevoir le client réservé ; une fonction qui appelle discrètement pool.query() rompt cette limite. node-postgres : transactions.
Vérifier l’instantané dans deux sessions Terminal
Un script rapide peut se terminer avant qu’une autre session ne modifie quoi que ce soit. Suivez l’exercice ci-dessous, dont l’ordre est délibéré, pour rendre explicite le moment important. Il s’agit d’étapes SQL propres à chaque session, utilisant la table déjà créée, et non de programmes de configuration indépendants. Les résultats attendus en base suivent le comportement d’isolation documenté ; ils ne sont pas présentés comme les résultats d’un test PostgreSQL exécuté.
Ouvrez deux fenêtres Terminal et exécutez psql -d postgres dans chacune. Avant de démarrer la lecture, rétablissez le sujet du jeu de données synthétique dans la session B. Attendez la fin de cette instruction :
-- Session B: restore the fixture before beginning the exercise.
UPDATE public.article_snapshot_demo
SET topic = 'craft'
WHERE id = 2;
Dans la session A, démarrez la transaction de lecture et récupérez la liste. Faites une pause une fois la requête terminée :
-- 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 liste doit contenir Paper Shapes et Color Studies. Passez maintenant à la session B et validez une modification de sujet au moyen de cette instruction autonome. En dehors d’un bloc de transaction explicite, PostgreSQL valide automatiquement toute instruction exécutée avec succès. PostgreSQL : BEGIN.
-- Session B: run while session A remains open.
UPDATE public.article_snapshot_demo
SET topic = 'design'
WHERE id = 2;
Revenez à la session A et terminez la lecture associée :
-- Session A: finish the original reader transaction.
SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true;
COMMIT;
Le décompte attendu est 2, ce qui correspond à la liste précédemment obtenue dans la session A. Ensuite, une nouvelle exécution du script Node.js devrait renvoyer uniquement Paper Shapes et un total de "1". L’ancien résultat et le nouveau représentent chacun une vue cohérente.
Pour effectuer une comparaison contrôlée, répétez l’exercice dans son intégralité, y compris le rétablissement initial dans la session B, mais démarrez la session A avec BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;. Respectez la même pause et le même ordre de mise à jour. La première liste attendue contient deux articles, tandis que le décompte ultérieur est 1. Cette comparaison permet d’isoler le paramètre de transaction comme seule condition modifiée.
Choisir la bonne limite pour les lectures associées
Utilisez ce guide de décision pour adapter l’exemple. Déterminez d’abord quels résultats doivent concorder, puis choisissez l’opération de base de données la plus simple qui les fournit.
| Tâche du lecteur | Approche appropriée | Condition à préserver |
|---|---|---|
| Renvoyer une liste d’articles issue d’une seule requête ordinaire | Une requête peut utiliser READ COMMITTED |
Aucune requête ultérieure n’a besoin de partager son instantané. |
| Renvoyer une liste et un décompte obtenus par des requêtes distinctes | Une transaction REPEATABLE READ READ ONLY |
Utiliser un seul client et des filtres correspondants. |
| Récupérer plusieurs lots pour obtenir un résultat cohérent | Conserver les lectures dans la même transaction | Une nouvelle transaction établit une nouvelle vue. |
| Coordonner des lectures entre des sessions de base de données distinctes | Exportation et importation explicites d’un instantané | Les transactions indépendantes ne partagent pas automatiquement un instantané. |
Pour plusieurs sessions, PostgreSQL fournit pg_export_snapshot() et SET TRANSACTION SNAPSHOT. La session qui exporte l’instantané doit rester ouverte jusqu’à ce que les autres transactions l’importent. Une transaction qui importe l’instantané doit utiliser REPEATABLE READ ou SERIALIZABLE et effectuer l’importation avant sa première requête ou instruction de modification des données. L’importation en mode sérialisable est soumise à des restrictions supplémentaires décrites dans la référence des commandes. PostgreSQL : fonctions de synchronisation des instantanés, PostgreSQL : SET TRANSACTION.
Pour cette tâche de liste et de décompte sur ordinateur de bureau, conserver un seul client évite cette coordination supplémentaire. Terminez les lectures de la base et fermez la transaction avant d’attendre qu’une personne choisisse une autre action.
Que laisse ce niveau d’isolation sans solution ?
REPEATABLE READ de PostgreSQL empêche les lectures fantômes, mais des anomalies de sérialisation restent possibles. Un instantané stable ne garantit donc pas que les opérations concurrentes se sont comportées comme si elles avaient été exécutées l’une après l’autre. Il ne fige pas non plus les valeurs de séquence, dont le comportement transactionnel est particulier. PostgreSQL : isolation des transactions.
READ ONLY interdit les modifications des tables ordinaires, sous réserve d’exceptions documentées telles que certaines opérations sur des tables temporaires. Cette option ne choisit pas à elle seule un instantané stable. Définissez les deux caractéristiques avant toute lecture : PostgreSQL ne vous permettra pas de modifier le niveau d’isolation après la première requête ou instruction de modification des données de la transaction. PostgreSQL : SET TRANSACTION.
Lorsqu’un rapport exige des garanties de sérialisabilité plus fortes, PostgreSQL prend en charge SERIALIZABLE READ ONLY DEFERRABLE. La transaction peut attendre l’obtention de son instantané, puis s’exécute sans annulation due à un échec de sérialisation. Ce compromis diffère du besoin ordinaire de lecture d’articles illustré ici. PostgreSQL : SET TRANSACTION.
Pour la liste et le décompte des articles, délimitez clairement l’implémentation : réserver un client, démarrer avec les caractéristiques requises, lire tous les résultats associés, valider la transaction, puis libérer le client. Réutilisez les mêmes filtres et ne publiez le résultat assemblé qu’une fois la transaction terminée. La prochaine action sur ordinateur de bureau disposera ainsi d’un ensemble cohérent de données d’articles.
Sources
- Isolation des transactions — Groupe de développement mondial PostgreSQL.
- BEGIN — Groupe de développement mondial PostgreSQL.
- SET TRANSACTION — Groupe de développement mondial PostgreSQL.
- Fonctions d’administration système : fonctions de synchronisation des instantanés — Groupe de développement mondial PostgreSQL.
- Transactions — node-postgres.
- API du pool — node-postgres.
- Connexion — node-postgres.
- Bienvenue — node-postgres.