14/08/2023

2. Clés étrangères

Lu 310 fois Licence Creative Commons

Normalisation

La table vehicule créée précédemment comporte une erreur dans sa conception. En effet, elle ne respecte pas la 3FN: la marque dépend du modele et on a donc une redondance dans les noms de marques. En cas de changement de nom, il faudrait donc modifier toutes les entrées de la table vehicule qui correspondent à la marque à renommer. En cas d'oubli, les données ne sont plus fiables.

La résolution de cette erreur de normalisation consiste en la création d'une nouvelle table dédiée aux marques:

modele (1, 1) - (1, n) marque
Un modèle correspond à 1 marque et une marque crée au moins un modèle. Les cardinalités maximales (à droite) constituent une relation OneToMany.

On va alors créer une table marque qui sera référencée dans la table vehicule par sa clé primaire, il s'agira alors d'une clé étrangère:

marque:

id nom
1 Renault
2 Peugeot
3 Toyota

vehicule:

id modele marque_id
1 Clio 1
2 Trafic 1
3 206 2
4 Partner 2
5 Auris 3

Correction

Plutôt que de supprimer la table et de recommencer, essayons de corriger cette erreur sans perdre les données. C'est un scénario qu'il est important de maîtriser dans le cas d'une application en production.

On crée la nouvelle table des marques avec sa séquence:

CREATE SEQUENCE seq_marque_id INCREMENT BY 1 MINVALUE 1 START 1;
CREATE TABLE marque (
    id INT NOT NULL,
    nom VARCHAR(20) NOT NULL,
    PRIMARY KEY (id)
);

Puis on insère les marques déjà existantes en se basant sur les entrées de la table vehicule:

INSERT INTO marque (id, nom)
SELECT nextval('seq_marque_id'), marque
FROM (SELECT DISTINCT marque FROM vehicule) v;

Enfin, on ajoute une nouvelle colonne marque_id dans la table vehicule qui va référencer la clé primaire de la table marque via une contrainte de clé étrangère. Cette contrainte va obliger à ce que les valeurs de vehicule.marque_id existent bien dans marque.id.

-- création de la nouvelle colonne avec NULL en valeur par défaut pour les lignes existantes
ALTER TABLE vehicule ADD COLUMN marque_id INT DEFAULT NULL;

-- remplissage de la nouvelle colonne avec les ID des marques correspondantes
UPDATE vehicule v
SET marque_id = m.id
FROM marque m
WHERE v.marque = m.nom;

-- nettoyage des colonnes et création de la contrainte de clé étrangère
ALTER TABLE vehicule
    ALTER COLUMN marque_id SET NOT NULL
    , DROP COLUMN marque
    , ADD CONSTRAINT fk_vehicule_marque FOREIGN KEY (marque_id) REFERENCES marque (id);

Désormais l'insertion d'une ligne dans la table vehicule échouera si la valeur de marque_id n'existe pas dans la table marque:

INSERT INTO vehicule (id, modele, marque_id) VALUES (7, 'Wrangler', 999);

[23503] ERROR: insert or update on table "vehicule" violates foreign key constraint "fk_vehicule_marque"
Detail: Key (marque_id)=(999) is not present in table "marque".

Événements

En dehors de l'insertion, les contraintes de clés étrangères peuvent affecter les autres opérations d'écriture: la modification et suppression des entrées référencées (table "parent"). On peut adapter le comportement du SGBD selon nos besoins pour empêcher l'opération ou la répercuter sur les entrées qui référencent (table "enfant").

Essayons de supprimer une ligne de la table marque:

DELETE FROM marque WHERE nom = 'Toyota';

[23503] ERROR: update or delete on table "marque" violates foreign key constraint "fk_vehicule_marque" on table "vehicule"
Detail: Key (id)=(2) is still referenced from table "vehicule".

L'opération est bloquée car des entrées de vehicule référencent actuellement la marque Toyota. Le comportement à la suppression est par défaut NO ACTION, qui est pratiquement identique à RESTRICT. On peut également définir une valeur avec SET NULL et SET DEFAULT, ou encore reproduire l'opération sur les entrées "enfant" avec CASCADE: en supprimant une marque, on peut supprimmer les vehicule associés.

Remplaçons la contrainte de clé étrangère dans vehicule:

ALTER TABLE vehicule
    DROP CONSTRAINT fk_vehicule_marque
    , ADD CONSTRAINT fk_vehicule_marque FOREIGN KEY (marque_id) REFERENCES marque (id) ON DELETE CASCADE;

On peut réessayer de supprimer une marque:

DELETE FROM marque WHERE nom = 'Toyota';

1 row affected in 9 ms