From 1a06ea5bf26d382fe168f1f21152b776907cdd63 Mon Sep 17 00:00:00 2001 From: medina5 Date: Sun, 2 Nov 2025 20:50:39 +0100 Subject: [PATCH] 5 banque --- banque.correction.md | 92 ------------------- banque.md | 209 +++++++++++++++++++++++++++++++++++++++++++ banque.sql | 122 ++++++++++++++++++++++++- 3 files changed, 329 insertions(+), 94 deletions(-) diff --git a/banque.correction.md b/banque.correction.md index b47ca7f..055402f 100644 --- a/banque.correction.md +++ b/banque.correction.md @@ -2,76 +2,13 @@ ## 1. Les titulaires -`holder` : table commune à tous les titulaires - -- identifiant unique (`id`) -- type de titulaire (`type` = 'PERSON' ou 'COMPANY') -- date de création - -```sql -create table holder ( - id bigint primary key generated always as identity, - type text not null check (type in ('PERSON', 'COMPANY')), - created_at timestamp not null default now() -); -``` `person` : informations propres aux personnes physiques - prénom, nom, date de naissance -```sql -create table person ( - id bigint primary key references holder(id) on delete cascade, - firstname text not null, - lastname text not null, - birthdate date not null -); -``` - `company` : informations propres aux entreprises - raison sociale, numéro d’immatriculation, date de création -```sql -create table company ( - id bigint primary key references holder(id) on delete cascade, - name text not null, - registration_number text unique not null, - created_at date not null -); -``` - -### Tests d'insertion des données - -```sql --- Création d’un titulaire de type personne physique -insert into holder(type) values ('PERSON') returning id; - --- Utilisons l'id qui est retourné (1) -insert into person(id, firstname, lastname, birthdate) - values (1, 'Françoise', 'Zanetti', '1995-04-12'); -``` - -```sql --- Création d’un titulaire de type entreprise -insert into holder(type) values ('COMPANY') returning id; - --- Utilisons l'id qui est retourné (2) -insert into company(id, name, registration_number, created_at) - values (2, 'Boulangerie de Valorgue', 'FR19803269968', '2014-08-19'); -``` - -### Vérification - -```sql -create view list_holders as -select h.id, h.type, h.created_at, - p.firstname || ' ' || p.lastname as person, - c.name as company -from holder h -left join person p on p.id = h.id -left join company c on c.id = h.id -order by h.id; -``` #### 1.5.1 Pourquoi séparer `person` et `company` ? @@ -113,32 +50,3 @@ create trigger trg_check_person_age before insert or update on person for each row execute procedure check_person_age(); ``` - -### 2. Les comptes - -```sql -create table account ( - id bigint primary key generated always as identity, - number text unique not null, - opened_at date not null default current_date, - closed_at date, - balance numeric(18,6) not null default 0 check (balance >= 0) -); -``` - -Chaque compte a un numéro unique. -La contrainte check (balance >= 0) empêche les soldes négatifs. - -```sql -create table account_holder ( - account_id int not null references account(id) on delete cascade, - holder_id int not null references holder(id) on delete cascade, - share numeric(4,3) check (share > 0 and share <= 1), - primary key (account_id, holder_id) -); -``` - -Cette table établit la relation n–n entre account et holder. -La contrainte share assure que les parts sont comprises entre 0 et 1. - -Un compte joint correspond donc à plusieurs lignes dans cette table. diff --git a/banque.md b/banque.md index eff1973..d04a02d 100644 --- a/banque.md +++ b/banque.md @@ -71,3 +71,212 @@ La banque souhaite désormais que toute personne titulaire d’un compte ait au - Créez un compte individuel pour Françoise Zanetti. - Ajouter un nouveau titulaire : Justin Hébrard né le 11/03/1993. - Créez un compte joint pour Françoise et Justin. + +## 3. L'intégrité des données + +Lorsque l’on tente d'insèrer une nouvelle personne qui n'a pas l'âge requis. La ligne dans holder est d'abord créée, puis l'insertion dans person échoue à cause de la vérification d'âge. Mais la ligne holder est toujours présente sans être rattachée à une personne. + +Chaque commande SQL est exécutée indépendamment. Si la deuxième commande échoue, la première n’est pas annulée automatiquement. + +Réalisez l’insertion d’un titulaire complet (dans holder et person) à l’aide d’une transaction. +Testez le cas où la contrainte d’âge échoue et vérifiez que rien n’est inséré dans holder. + +## 4. Procédure stockée + +Vous avez remarqué qu’en ajoutant une contrainte sur l’âge minimum du titulaire, une insertion invalide dans `person` peut échouer **après** la création du `holder`. +Cela laisse un **titulaire orphelin** sans fiche personnelle. + +Dans un système bancaire réel, ce type d’incohérence est inacceptable. +Nous allons donc **encapsuler la création d’un titulaire dans une procédure stockée** pour garantir la cohérence et la simplicité d’usage. + +1. Créez une **procédure stockée** appelée `create_person_holder` + + * qui prend en paramètre : + + * le prénom (`p_firstname text`) + * le nom (`p_lastname text`) + * la date de naissance (`p_birthdate date`) + +2. Cette procédure doit : + + * Vérifier que la personne a **au moins 15 ans** ; + * Créer automatiquement un enregistrement dans `holder` (type = `'PERSON'`) ; + * Récupérer l’identifiant généré et créer la fiche dans `person` ; + * Afficher un message de confirmation avec `RAISE NOTICE`. + +3. Si la personne a moins de 15 ans : + + * La procédure doit **refuser la création** et afficher une erreur claire (par `RAISE EXCEPTION`). + + +En PL/pgSQL les **procédures** s’écrivent : + + ```sql + create or replace procedure nom_procedure(paramètres) + language plpgsql + as $$ + declare + -- variables locales + begin + -- instructions SQL + end; + $$; + ``` + +Vous pouvez intercepter une erreur métier à l’aide de : + + ```sql + raise exception 'message'; + ``` + +Pour afficher un message de réussite : + + ```sql + raise notice 'message %', variable; + ``` + +Pour appeler une procédure stockée + +```sql +call create_person_holder('Alice', 'Martin', '1990-03-15'); +``` + +>[!NOTE] +> La procédure garantit l’**atomicité** : soit tout est créé, soit rien. + +## 5. Cohérence des données + +Écrire une requête pour vérifier la somme + +Excellent 👌 — on va continuer dans la même logique de rigueur comptable et de progressivité pédagogique. +Cette étape introduira une **seconde procédure stockée**, dédiée à la création de comptes, avec **contrôle des titulaires associés et de leurs parts (shares)**. + +--- + +# 🧩 TP Banque – Étape 5 : Créer un compte bancaire de manière cohérente + +--- + +## 🎯 Objectif + +Jusqu’à présent, nous savons créer des titulaires (`holder`) de façon sûre. +Il est temps de leur ouvrir des **comptes bancaires**. + +Un compte doit toujours : + +* être associé à **au moins un titulaire**, +* et la somme des parts (`share`) des titulaires doit être exactement **égale à 1** (c’est-à-dire 100 % du compte). + +Nous allons donc écrire une **procédure stockée** `create_account` qui vérifie ces règles avant d’enregistrer les données. + +--- + +## 📘 Contexte + +Les tables concernées sont les suivantes : + +```sql +create table account ( + id serial primary key, + iban text not null unique, + name text not null, + opened_on date not null default current_date +); + +create table account_holder ( + account_id int not null references account(id) on delete cascade, + holder_id int not null references holder(id) on delete restrict, + share numeric(5,4) not null check (share > 0 and share <= 1), + primary key (account_id, holder_id) +); +``` + +👉 La table `account_holder` relie un compte à un ou plusieurs titulaires, avec la part de chacun. + +--- + +## 🧱 Travail demandé + +1. Créez une **procédure stockée** `create_account` qui : + + * prend en paramètre : + + * `p_iban text`, + * `p_name text`, + * `p_holders int[]` (tableau des identifiants de titulaires), + * `p_shares numeric[]` (tableau des parts correspondantes). + * vérifie que : + + * le nombre d’éléments dans `p_holders` et `p_shares` est identique ; + * la somme des parts est **exactement égale à 1** ; + * chaque `holder_id` existe dans la table `holder`. + +2. Si tout est correct : + + * crée un nouveau compte dans `account` ; + * insère les lignes correspondantes dans `account_holder`. + +3. Si une condition échoue : + + * la procédure doit lever une **erreur explicite** (`RAISE EXCEPTION`) ; + * aucune insertion ne doit être faite (transaction annulée). + +--- + +## 💡 Aide + +* Les tableaux peuvent être parcourus avec une boucle : + + ```sql + for i in 1..array_length(p_holders, 1) loop + ... + end loop; + ``` + +* Pour vérifier la somme : + + ```sql + select sum(unnest(p_shares)); + ``` + + (ou additionner dans la boucle) + +* Vous pouvez lever une erreur personnalisée : + + ```sql + raise exception 'La somme des parts doit être égale à 1 (%.4f)', somme; + ``` + +--- + +## ✅ Exemple d’appel + +### Cas valide : + +```sql +call create_account( + 'FR761234567890', + 'Compte commun', + array[1, 2], + array[0.5, 0.5] +); +``` + +➡️ Crée un compte partagé 50/50 entre les titulaires 1 et 2. + +### Cas invalide : + +```sql +call create_account( + 'FR009999999999', + 'Compte déséquilibré', + array[1, 2], + array[0.7, 0.4] +); +``` + +❌ Doit refuser la création avec une erreur claire : + +``` +ERROR: La somme des parts (1.1000) doit être égale à 1.0000 +``` diff --git a/banque.sql b/banque.sql index 12c2e1b..9859ea5 100644 --- a/banque.sql +++ b/banque.sql @@ -53,7 +53,7 @@ insert into company(id, name, registration_number, created_at) */ -- Liste de tous les titulaires -create view list_holders as +create view holders_list as select h.id, h.type, h.created_at, p.firstname || ' ' || p.lastname as person, c.name as company @@ -86,7 +86,7 @@ check (birthdate <= current_date - interval '15 years'); insert into holder(type) values ('PERSON') returning id; insert into person(id, firstname, lastname, birthdate) - values (3, 'Mattéo', 'Zanetti', '2012-12-12'); + values (4, 'Mattéo', 'Zanetti', '2012-12-12'); /* * Account @@ -146,3 +146,121 @@ insert into account_holder insert into account_holder values (2, 5, 0.5); +create view accounts_list as +select + a.number, + h.type, + coalesce(p.firstname || ' ' || p.lastname, c.name) as holder_name, + ah.share, + a.balance +from account a +join account_holder ah on ah.account_id = a.id +join holder h on h.id = ah.holder_id +left join person p on p.id = h.id +left join company c on c.id = h.id +order by a.id, h.id; + +/* + * Une transaction + * L'enregistrement réel de toutes les entités s'effectue au commit + */ + +begin; + +insert into holder(type) values ('PERSON') returning id; + +insert into person(id, firstname, lastname, birthdate) + values (6, 'Justin', 'Hébrard', '1993-03-11'); + +commit; + + +create or replace procedure create_person_holder( + p_firstname text, + p_lastname text, + p_birthdate date +) +language plpgsql +as $$ +declare + v_holder_id bigint; +begin + -- Vérification métier + if p_birthdate > current_date - interval '15 years' then + raise exception 'Le titulaire doit avoir au moins 15 ans (%).', p_birthdate + using errcode = 'P0001'; + end if; + + -- Création atomique du titulaire + insert into holder(type) + values ('PERSON') + returning id into v_holder_id; + + insert into person(id, firstname, lastname, birthdate) + values (v_holder_id, p_firstname, p_lastname, p_birthdate); + + raise notice 'Titulaire créé avec succès : %, % % (ID=%)', + p_firstname, p_lastname, to_char(p_birthdate, 'YYYY-MM-DD'), v_holder_id; +end; +$$; + +call create_person_holder('Mattéo', 'Zanetti', '2012-12-12'); +call create_person_holder('Clotilde', 'Hébrard', '1989-07-14'); + +select account_id, sum(share) as total_share +from account_holder +group by account_id; + +create or replace procedure create_account( + p_iban text, + p_name text, + p_holders int[], + p_shares numeric[] +) +language plpgsql +as $$ +declare + v_account_id int; + v_sum numeric(6,4) := 0; + v_count int; +begin + -- Vérification des tailles + if array_length(p_holders, 1) is null or array_length(p_shares, 1) is null then + raise exception 'Les tableaux de titulaires et de parts ne peuvent pas être vides.'; + end if; + + if array_length(p_holders, 1) <> array_length(p_shares, 1) then + raise exception 'Les tableaux de titulaires et de parts doivent avoir la même taille.'; + end if; + + -- Calcul de la somme des parts + for i in 1..array_length(p_shares, 1) loop + v_sum := v_sum + p_shares[i]; + end loop; + + if abs(v_sum - 1.0) > 0.0001 then + raise exception 'La somme des parts (%.4f) doit être égale à 1.0000', v_sum; + end if; + + -- Vérification des titulaires + select count(*) into v_count from holder where id = any(p_holders); + if v_count <> array_length(p_holders, 1) then + raise exception 'Un ou plusieurs titulaires n''existent pas.'; + end if; + + -- Création du compte + insert into account(iban, name) + values (p_iban, p_name) + returning id into v_account_id; + + -- Association des titulaires + for i in 1..array_length(p_holders, 1) loop + insert into account_holder(account_id, holder_id, share) + values (v_account_id, p_holders[i], p_shares[i]); + end loop; + + raise notice 'Compte créé avec succès (ID=%) avec % titulaires.', + v_account_id, array_length(p_holders, 1); +end; +$$; +