Aujourd’hui, notre objectif c’est de coder une fonction qui :
- Vérifie le solde du client. Avant de débiter une somme, il faut checker si le client a assez d’argent.
- Débite le solde. Si le solde est suffisant, on fait le débit.
- Log toutes les opérations, réussies ou pas. Toutes les actions sont enregistrées dans une table de logs pour pouvoir les analyser plus tard.
C’est pas juste une fonction barbante qui fait une soustraction. Ici, on va utiliser des transactions imbriquées pour pouvoir annuler les changements si jamais un truc foire (genre pas assez d’argent ou erreur lors de l’écriture du log). On va découvrir l’utilité des points de sauvegarde (SAVEPOINT) et apprendre à rendre nos procédures résistantes aux bugs.
Création des tables de base
Avant de se lancer dans la fonction, on prépare la base de données. Il nous faut trois tables :
clients— pour stocker les infos des clients et leur solde.payments— pour enregistrer les transactions réussies.logs— pour garder la trace de toutes les tentatives de paiement (réussies ou pas).
-- Table des clients
CREATE TABLE clients (
client_id SERIAL PRIMARY KEY,
full_name TEXT NOT NULL,
balance NUMERIC(10, 2) NOT NULL DEFAULT 0
);
-- Table des paiements réussis
CREATE TABLE payments (
payment_id SERIAL PRIMARY KEY,
client_id INT NOT NULL REFERENCES clients(client_id),
amount NUMERIC(10, 2) NOT NULL,
payment_date TIMESTAMP DEFAULT NOW()
);
-- Table des logs
CREATE TABLE logs (
log_id SERIAL PRIMARY KEY,
client_id INT NOT NULL REFERENCES clients(client_id),
message TEXT NOT NULL,
log_date TIMESTAMP DEFAULT NOW()
);
On va remplir la table clients avec des données de test
INSERT INTO clients (full_name, balance)
VALUES
('Otto Song', 100.00),
('Maria Chi', 50.00),
('Anna Vel', 0.00);
Maintenant on a trois clients : Otto a 100 sur son compte, Maria en a 50, et Anna a 0.
Implémentation de la logique métier : PROCÉDURE vs FONCTION
En bref :
- Pour les opérations business « tout ou rien », une fonction suffit.
- Pour gérer les transactions étape par étape, faire des commits partiels, des rollbacks, logger les erreurs — utilise une procédure (
CREATE PROCEDURE).
Pourquoi pas une fonction ? Parce que dans PostgreSQL 17, à l’intérieur d’une fonction tu NE peux PAS utiliser COMMIT, SAVEPOINT ou ROLLBACK. Tout est fait de façon atomique dans la transaction externe.
Seule une procédure (CREATE PROCEDURE ... LANGUAGE plpgsql) te permet d’utiliser SAVEPOINT, COMMIT, ROLLBACK — mais avec des limitations importantes :
- Dans une procédure, tu peux utiliser
SAVEPOINT,COMMIT,RELEASE SAVEPOINT. ROLLBACK TO SAVEPOINTest interdit en PL/pgSQL (ça fait une erreur), à la place on utilise les blocsBEGIN ... EXCEPTION ... ENDqui font un « savepoint virtuel ».
La technique principale pour annuler une partie du code :
BEGIN
-- ton code ici
EXCEPTION
WHEN OTHERS THEN
-- Ce bloc annule TOUS les changements faits à l’intérieur en cas d’erreur !
-- Tu peux laisser une trace dans le log :
INSERT INTO logs (...) VALUES (...);
END;
Créons une procédure de paiement avec rollback partiel et logging
CREATE OR REPLACE PROCEDURE process_payment(
in_client_id INT,
in_payment_amount NUMERIC
)
LANGUAGE plpgsql
AS $$
DECLARE
current_balance NUMERIC;
BEGIN
-- On récupère le solde du client
SELECT balance INTO current_balance
FROM clients
WHERE client_id = in_client_id;
IF NOT FOUND THEN
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Client non trouvé, opération refusée');
RAISE EXCEPTION 'Client avec ID % non trouvé', in_client_id;
END IF;
-- On vérifie que le solde est suffisant
IF current_balance < in_payment_amount THEN
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Solde insuffisant pour débiter ' || in_payment_amount || ' €.');
-- On arrête la procédure
RETURN;
END IF;
-- Bloc pour les changements atomiques ; en cas d’erreur — rollback (savepoint virtuel)
BEGIN
-- On débite le solde
UPDATE clients
SET balance = balance - in_payment_amount
WHERE client_id = in_client_id;
-- On ajoute une ligne pour le paiement réussi
INSERT INTO payments (client_id, amount)
VALUES (in_client_id, in_payment_amount);
-- On log le succès
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Débit réussi de ' || in_payment_amount || ' €.');
EXCEPTION
WHEN OTHERS THEN
-- Tous les changements dans ce bloc sont annulés
INSERT INTO logs (client_id, message)
VALUES (in_client_id, 'Erreur lors du paiement : ' || SQLERRM);
-- (pas besoin de ROLLBACK TO SAVEPOINT — c’est interdit et inutile)
END;
END;
$$;
En résumé, ce qui se passe :
- Si le solde est insuffisant ou le client n’existe pas — on log et on sort.
- Tout le code critique est dans un bloc
BEGIN ... EXCEPTION ... END. - Si une erreur arrive dans ce bloc — tous les changements sont annulés automatiquement ; on écrit l’erreur dans les logs.
- Pas d’utilisation directe de
SAVEPOINTniROLLBACK TO SAVEPOINT— c’est normal, en PL/pgSQL ça marche uniquement via les blocs EXCEPTION.
Appeler la procédure
Important : il faut appeler la procédure avec la commande CALL ..., et la connexion à la base doit être en mode autocommit ou hors d’une grosse transaction explicite !
CALL process_payment(1, 30.00); -- Paiement réussi
CALL process_payment(2, 100.00); -- Solde insuffisant
CALL process_payment(999, 50.00); -- Client inexistant
Vérifier les résultats
- Le solde du client change seulement si le paiement passe.
- La table payments — une ligne seulement si le débit a réussi.
- logs — l’historique de toutes les tentatives (et erreurs).
SELECT * FROM clients;
SELECT * FROM payments;
SELECT * FROM logs;
Application réelle
Les procédures pour gérer les transactions sont au cœur des systèmes fintech, e-commerce et même des plateformes de jeux. Imagine une boutique en ligne qui doit gérer le solde des cartes cadeaux et les débiter lors des achats, ou une banque avec des milliers d’opérations par seconde.
Tout ça, ça va vraiment te servir en pratique, ça t’aidera à sécuriser les données de tes clients et à éviter des bugs catastrophiques dans la gestion des paiements.
GO TO FULL VERSION