5 banque
This commit is contained in:
@@ -2,76 +2,13 @@
|
|||||||
|
|
||||||
## 1. Les titulaires
|
## 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
|
`person` : informations propres aux personnes physiques
|
||||||
- prénom, nom, date de naissance
|
- 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
|
`company` : informations propres aux entreprises
|
||||||
- raison sociale, numéro d’immatriculation, date de création
|
- 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` ?
|
#### 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
|
before insert or update on person
|
||||||
for each row execute procedure check_person_age();
|
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.
|
|
||||||
|
|||||||
209
banque.md
209
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.
|
- Créez un compte individuel pour Françoise Zanetti.
|
||||||
- Ajouter un nouveau titulaire : Justin Hébrard né le 11/03/1993.
|
- Ajouter un nouveau titulaire : Justin Hébrard né le 11/03/1993.
|
||||||
- Créez un compte joint pour Françoise et Justin.
|
- 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
|
||||||
|
```
|
||||||
|
|||||||
122
banque.sql
122
banque.sql
@@ -53,7 +53,7 @@ insert into company(id, name, registration_number, created_at)
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
-- Liste de tous les titulaires
|
-- Liste de tous les titulaires
|
||||||
create view list_holders as
|
create view holders_list as
|
||||||
select h.id, h.type, h.created_at,
|
select h.id, h.type, h.created_at,
|
||||||
p.firstname || ' ' || p.lastname as person,
|
p.firstname || ' ' || p.lastname as person,
|
||||||
c.name as company
|
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 holder(type) values ('PERSON') returning id;
|
||||||
|
|
||||||
insert into person(id, firstname, lastname, birthdate)
|
insert into person(id, firstname, lastname, birthdate)
|
||||||
values (3, 'Mattéo', 'Zanetti', '2012-12-12');
|
values (4, 'Mattéo', 'Zanetti', '2012-12-12');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Account
|
* Account
|
||||||
@@ -146,3 +146,121 @@ insert into account_holder
|
|||||||
insert into account_holder
|
insert into account_holder
|
||||||
values (2, 5, 0.5);
|
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;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user