I. Posons le problème▲
Il y a en fait deux attitudes à avoir et aucune alternative : soit vous voulez mettre ces contraintes et dans ce cas ce sera toujours dans la base de données qu'il faudra les y placer ; ou alors vous ne voulez pas de contrainte d'intégrité référentielle et par conséquent elles n'ont à figurer nulle part, ni dans la base ni dans le code client.
J'entends déjà des protestations s'élever avec l'argument suivant : « Oui, mais on peut les mettre dans le code du programme client ». Et là je réponds : si vous voulez la pire des solutions, alors n'hésitez pas, amusez-vous à piloter les références de clefs étrangères directement dans le code et revenez me voir quand vous aurez des problèmes… Mon tarif actuel en matière d'audit est situé entre 1200 ? HT par jour et 1600… et ce genre de stupidité est mon pain quotidien ! Et je suis même en train d'augmenter mes tarifs, car je suis surbooké par les demandes d'audit de bases dont les performances sont lamentables causées par l'application de dogmes imbéciles tels que celui-là !
II. Des FOREIGN KEY vérifiées par code client…▲
Les personnes qui sortent l'argument stupide du « Mettez les contraintes FOREIGN KEY dans le code client » oublient toujours une chose fondamentale… Un SGBD relationnel est un système massivement concurrentiel et pour cela fortement parallèle. Si la base ne pouvait être utilisée que par un seul client à la fois, alors l'argument serait fort recevable et bien que je ne vous le conseille pas, je ne noterais pas dans mon rapport d'audit de faute grave. Je me contenterais de signaler qu'il n'y a pas, en général, qu'une seule application qui peut accéder à la base, mais toute une série, depuis les outils d'administration aux ETL ou outils d'import de données, et qu'il convient donc d'implanter ce code client de vérification des contraintes aussi dans ces outils-là, ce qui suppose que vous vous les fabriquiez spécifiquement, car rares sont les éditeurs sérieux qui pensent à le faire. Bref, il vous faudra donc tout redévelopper…
Étudions maintenant les choses de manière simple et directe avec un exemple concret. Soit les tables suivantes :
CREATE
TABLE
T_CLIENT
(
CLI_ID INT
NOT
NULL
PRIMARY
KEY
,
CLI_NOM VARCHAR
(
16
))
;
CREATE
TABLE
T_COMMANDE
(
COM_ID INT
NOT
NULL
PRIMARY
KEY
,
CLI_ID INT
,
COM_DATE DATE
)
;
Pour ne pas risquer d'insérer une commande d'un client qui n'existe pas, on serait tenté de faire dans le code client, la chose suivante :
Soit à insérer la facture n° 222 pour le client 987 à la date du jour :
1) lancer la requête suivante :
SELECT
*
FROM
T_CLIENT WHERE
CLI_ID =
987
2) vérifier s'il y a un résultat
IF
NOT
MyQuery.Empty()
THEN
3) lancer la requête :
INSERT
INTO
T_COMMANDE VALUES
(
222
, 987
, CURRENT_DATE
)
En principe tout devrait bien se passer… Sauf que c'est sans compter sur la sournoisité d'un SGBDR. Reprenons ce code avec un peu de concurrence, c'est-à-dire avec quelques dizaines d'utilisateurs qui font eux aussi des requêtes. Introduisons juste une seule petite requête en plein milieu de votre traitement, comme ceci :
DELETE
FROM
T_CLIENT WHERE
CLI_ID =
987
Et vous aurez compris que vous allez insérer une commande avec un client qui vient tout juste de disparaître !
Bien entendu, il est possible d'améliorer cela par exemple en proposant d'encapsuler ce traitement dans une transaction. Sauf qu'il va falloir utiliser un niveau d'isolation suffisant. En effet une transaction garantit l'aspect « tout ou rien » (atomicité), mais c'est à vous de savoir quel niveau de verrouillage vous souhaitez donner aux tables dans votre traitement. Or il existe quatre niveaux d'isolation :
- le niveau 0 dit « READ UNCOMITTED » propose de lire même des données qui ne sont pas valides. Par exemple un client que l'on vient d'insérer, mais qui va être annulé… Pas vraiment adapté à notre problématique !
- le niveau 1 dit « READ COMMITED » propose de ne lire que des données impérativement validées (COMMIT). C'est séduisant, mais inefficace. En effet rien n'empêchera quelqu'un de supprimer le client que vous venez de lire et de valider sa transaction avant que vous n'ayez le temps de l'insérer. En effet dans ce mode les verrous sont posés sur les tables pour chaque ordre SQL et libérés immédiatement après ;
- le niveau 2 dit « REPEATABLE READ » assure que les données que vous lisez une première fois puissent être relues avec les mêmes valeurs à chaque lecture. C'est bien ce mode qu'il faut utiliser dans le cas présent. Or ce mode possède un petit inconvénient : il pose des verrous de ligne sur chacune des tables des commandes SQL de la transaction et ne les libère que passé le COMMIT ou le ROLLBACK !
- le niveau 3 dit « SERIALIZABLE » empêche toute mise à jour (INSERT, UPDATE, DELETE) dans les tables visées, sauf par vous bien entendu. Autant prendre un bulldozer pour tailler les données de la base ! En effet, il faut pour cela verrouiller le contenu intégral de chacune des tables…
Vous venez donc de comprendre que le code à exécuter pour être sûr de votre fait est bien plus complexe que cela. Pire, il est ultra contreperformant… En effet, démarrer une transaction qui maintient des verrous durant tout le traitement, et cela depuis le client, rend la transaction longue, très longue, en regard de l'exécution des commandes SQL, car le temps réseau est systématiquement l'un des plus longs qui soient. Entre les quelques microsecondes que vont durer vos requêtes et le temps de faire transiter tous les messages entre le client et le serveur dans les trames réseau dans un système non déterministe (Ethernet), il n'y a pas photo… on peut estimer que 95 % du temps sera consacré à bavarder et non à traiter les données.
Finalement le code client devrait ressembler à cela :
1) lancer la requête suivante :
SET
TRANSACTION
ISOLATION
LEVEL
REPEATABLE
READ
2) lancer la requête suivante :
BEGIN
TRANSACTION
3) lancer la requête suivante :
SELECT
*
FROM
T_CLIENT WHERE
CLI_ID =
987
4) vérifier s'il y a un résultat et pas d'erreur
IF
NOT
exception
()
AND
NOT
MyQuery.Empty()
THEN
5) lancer la requête :
INSERT
INTO
T_COMMANDE VALUES
(
222
, 987
, CURRENT_DATE
)
6) s'il n'y a pas d'erreur, lancer la requête :
COMMIT
TRANSACTION
7) en cas d'erreur, lancer la requête :
ROLLBACK
TRANSACTION
8) dans tous les cas, revenir au mode d'isolation par défaut (en principe READ COMMITTED)
SET
TRANSACTION
ISOLATION
LEVEL
READ
COMMITTED
Bref, énorme et totalement contreperformant, donc parfaitement imbécile. De plus, il y a fort à parier que la plupart des développeurs oublieront tout ce cinéma et par conséquent, on se retrouvera devant une base désintégrée !
Il y a évidemment le moyen de faire ceci sans transaction… En effet, avec un peu d'astuce, on peut y arriver, mais c'est aussi complexe et contreperformant :
1) créer une table temporaire locale pour y stocker les lignes à insérer :
CREATE
TEMPORARY
LOCAL
TABLE
TMP_T_COMMANDE
(
COM_ID INT
,
CLI_ID INT
,
COM_DATE DATE
)
2) lancer la requête suivante :
INSERT
INTO
TMP_T_COMMANDE VALUES
(
222
, 987
, CURRENT_DATE
)
3) lancer la requête suivante :
INSERT
INTO
T_COMMANDE
SELECT
*
FROM
T_TMP_COMMANDE
WHERE
CLI_ID IN
(
SELECT
CLI_ID
FROM
T_COMMANDE)
4) supprimer la table temporaire locale en lançant requête :
DROP
TABLE
TMP_T_COMMANDE
Voilà, il n'y a plus de transaction, mais le coût d'un tel traitement devient exorbitant. Parce que si l'on compte 1 comme mesure de coût pour un SELECT, 10 pour un INSERT, 30 pour une transaction et 100 pour un CREATE ou un DROP, alors on passe d'un coût de 50 à un coût de 260 !
Bref, c'est encore pire et plus complexe…
Vous noterez au passage que c'est dans la requête d'insertion que l'on fait le test, ce qui revient strictement à la même chose si nous avions implanté les contraintes d'intégrité référentielle, mais en pire bien entendu !
Vous venez donc de comprendre que : soit il faut implanter les contraintes d'intégrité référentielles dans la base, soit ne pas les implanter du tout !
III. C'est lent▲
Détruisons maintenant l'argument de la supposée lenteur…
Une contrainte SQL est vérifiée avant même que les données soient mises à jour. Pour les contraintes NOT NULL et DEFAULT, pour chaque valeur. Pour les autres contraintes, pour la ligne entière de données, mais avant toute insertion, modification ou suppression. Autrement dit, il ne s'agit que vérifier, pas encore de faire… Alors que dans le code que nous venons de voir il faut commencer à faire pour défaire…
À votre avis, quelle solution est la plus rapide et quelle est la plus lente ?
Bref, il est vrai que toute contrainte a un coût, mais bien moindre que de reproduire le mécanisme côté client.
Mais les contraintes peuvent-elles augmenter les performances d'un SGBDR ?
Chez certains éditeurs, c'est le cas. En effet Oracle ou SQL Server se servent des contraintes mises en place dans la base pour trouver la meilleure façon de faire telle ou telle requête. Analysons ce phénomène avec un exemple plus détaillé…
Soit les tables suivantes :
T_ADHERENT
ADH_ID |
ADH_NOM |
ADH_PRENOM |
… |
1 |
Philippe |
Noiret |
|
2 |
Jean |
Poiret |
|
3 |
Michel |
Serrault |
|
4 |
Jacques |
Villeret |
|
… |
… |
||
7892 |
Marcel |
Dupont |
Notez que tous les adhérents de cette association sportive ne figurent pas exhaustivement, mais que nous en avons dénombré 7892 !
T_SPORT
SPT_ID |
SPT_LIBELLE |
1 |
Escrime |
5 |
Équitation |
12 |
Tennis |
18 |
Natation |
23 |
Course à pied |
47 |
Ping-pong |
52 |
Golf |
78 |
Volley |
85 |
Hand-ball |
112 |
Football |
823 |
Rugby |
9845 |
Moto |
Ici, la liste des sports est exhaustive…
T_PRATIQUE
ADH_ID |
SPT_ ID |
1 |
5 |
1 |
12 |
2 |
1 |
2 |
52 |
3 |
18 |
3 |
78 |
3 |
85 |
3 |
33 |
4 |
5 |
4 |
52 |
… |
|
7892 |
112 |
7892 |
18 |
7892 |
23 |
8954155 |
52 |
Là, force est de constater que nous manquons de place pour exprimer les 27 011 occurrences de cette table…
Admettons enfin que tous nos échantillons ici présentés sont parfaitement représentatifs.
La requête suivante :
SELECT
DISTINCT
SPT_LIBELLE
FROM
T_PRATIQUE AS
P
INNER
JOIN
T_ADHERENT AS
A
ON
P.ADH_ID =
A.ADH_ID
INNER
JOIN
T_SPORT AS
S
ON
P.SPT_ID =
S.SPT_ID
permet de savoir quels sont les sports réellement pratiqués dans ce club. Bien entendu, un sport pratiqué veut dire que :
- ce sport existe dans la liste des sports ;
- cet adhérent existe dans la liste des adhérents.
Si une contrainte d'intégrité de référence avait été posée entre le couple de tables T_PRATIQUE / T_ADHERENT en vérifiant l'existence des noms et prénoms des adhérents lors de la saisie dans la table T_PRATIQUE et que de même, on avait posé une intégrité de référence entre le couple de tables T_PRATIQUE / T_SPORT en vérifiant l'existence de chaque sport lors de la saisie dans la table T_PRATIQUE, alors on aurait évité les erreurs de saisies comme :
ADH_ID |
SPT_LIBELLE |
3 |
33 |
8954155 |
52 |
En effet, ni le sport « 33 » n'existe dans la table T_SPORT, ni l'adhérent 8954155 n'existe dans la table T_ADHERENT (peut être ont-ils réellement existé quelques années auparavant !).
Avec de telles contraintes, il était alors possible de simplifier grandement la requête en :
SELECT
DISTINCT
SPT_LIBELLE
FROM
T_PRATIQUE AS
P
INNER
JOIN
T_SPORT AS
S
ON
P.SPT_ID =
S.SPT_ID
Bien évidemment, cette dernière requête est incommensurablement plus performante : elle travaille sur un jeu de données moindre et n'a qu'une jointure à faire ! Et que pensez-vous qu'un bon moteur relationnel soit capable de faire ? Justement ce genre de simplification. En effet un moteur de requêtes est un outil mathématique qui travaille par transformation afin de trouver une écriture allégée de la requête et la moins consommatrice de ressources… Encore faut-il que le moteur SQL soit bon !
Pour vous en convaincre, vous pouvez vous amuser à mesurer cela sur votre SGBDR favori à l'aide du script téléchargeable ici [Loading database.zip].
Et voici les essais effectués :
SET
STATISTICS IO
ON
;
-- requête originale
SELECT
DISTINCT
SPT_LIBELLE
FROM
T_PRATIQUE AS
P
INNER
JOIN
T_ADHERENT AS
A
ON
P.ADH_ID =
A.ADH_ID
INNER
JOIN
T_SPORT AS
S
ON
P.SPT_ID =
S.SPT_ID;
-- nombre d'IO pour la lecture des données de la requête sans contraintes
Table
'T_ADHERENT'
. lectures logiques 36
Table
'T_PRATIQUE'
. lectures logiques 222
Table
'T_SPORT'
. lectures logiques 2
-- nettoyage de la table des données parasites :
DELETE
FROM
T_PRATIQUE
WHERE
ADH_ID =
3
AND
SPT_ID =
33
;
DELETE
FROM
T_PRATIQUE
WHERE
ADH_ID =
8954155
AND
SPT_ID =
52
;
-- implantation des contraintes FK :
ALTER
TABLE
T_PRATIQUE
ADD
CONSTRAINT
FK_ADH
FOREIGN
KEY
(
ADH_ID)
REFERENCES
T_ADHERENT (
ADH_ID)
;
ALTER
TABLE
T_PRATIQUE
ADD
CONSTRAINT
FK_SPT
FOREIGN
KEY
(
SPT_ID)
REFERENCES
T_SPORT (
SPT_ID)
;
-- nouvelle requête optimisée :
SELECT
DISTINCT
SPT_LIBELLE
FROM
T_PRATIQUE AS
P
INNER
JOIN
T_SPORT AS
S
ON
P.SPT_ID =
S.SPT_ID;
-- nombre d'IO pour la lecture des données de la requête avec contraintes
Table
'T_SPORT'
. lectures logiques 20
Table
'Worktable'
. lectures logiques 0
Table
'T_PRATIQUE'
. lectures logiques 96
,
--> y a pas photo : 360 lectures contre 116, soit 3 fois mieux ! merci les contraintes !!!
IV. C'est contraignant▲
Eh oui, les contraintes sont faites… pour contraindre ! Donc, c'est contraignant. Et de ce fait, de nombreux utilisateurs se plaignent de la problématique des FOREIGN KEY.
- Comment donc reconstruire une base de données avec un script SQL où il y a des tables liées les unes aux autres par des contraintes d'intégrité référentielle ?
- Comment vider toutes les tables d'une base, ou bien insérer un jeu d'essai dans différentes tables sans être obligé de respecter un ordre précis, difficile à déterminer ?
Ces deux questions souvent posées dans les forums ont des réponses très simples.
Pour reconstruire une base de données avec toutes les contraintes d'intégrité référentielle deux moyens sont possibles :
- générer un script de création des tables sans les contraintes FOREIGN KEY et un autre avec des ALTER TABLE afin de remettre en place les contraintes d'intégrité référentielle, ce que font les bons outils de modélisation comme Power AMC ou les outils de rétro-ingénierie de bases de données comme SQL Server management Studio. Entre le passage des deux scripts, on peut insérer un script de chargement des données ;
- utiliser le principe de création de schéma SQL. J'ai indiqué comment faire dans un de mes blogs intitulé « De l'intérêt des SCHÉMA SQL » (http://blog.developpez.com/sqlpro/p5835/langage-sql-norme/de-l-interet-des-schema-sql/). L'avantage avec la création des objets au sein d'un schéma, c'est que vous n'avez plus à vous poser aucune question sur l'ordre d’enchaînement de la création des objets. C'est prévu par la norme SQL !
Pour vider toutes les tables d'une base de données ou bien insérer des données d'un jeu d'essais dans différentes tables, il suffit de désactiver ou supprimer les contraintes FOREIGN KEY puis de les remettre en place après le travail !
Certains SGBDR proposent une option (interdite par la norme SQL) pour désactiver les contraintes, genre ALTER TABLE … DISABLE CONSTRAINT. Je n'en suis pas fan ! Mieux vaut avoir sous la main un script de construction/suppression des contraintes FOREIGN KEY. C'est généralement simple à obtenir si vous avez un bon outil d'administration de votre SGBDR… Si ce n'est pas le cas, il faudra le générer par des requêtes dans les vues d'information de schéma.
Voici par exemple deux scripts SQL qui ajoutent et suppriment toutes les contraintes FOREIGN KEY d'une base de données, que l'on peut obtenir par exécution de la procédure dbo.P_DDL_FK_CONSTRAINTS pour MS SQL Server :
CREATE
PROCEDURE
dbo.P_DDL_FK_CONSTRAINTS @SQLCOMMAND VARCHAR
(
4
)
AS
BEGIN
IF
@SQLCOMMAND NOT
IN
(
'ADD'
, 'DROP'
)
RETURN
;
DECLARE
@SQL
VARCHAR
(
MAX
)
, @COLS_FK VARCHAR
(
MAX
)
, @COLS_UK VARCHAR
(
MAX
)
,
@TABLE_SCHEMA sysname, @TABLE_NAME
sysname,
@CONSTRAINT_SCHEMA
sysname, @CONSTRAINT_NAME
sysname,
@MATCH_OPTION NVARCHAR
(
7
)
,
@UPDATE_RULE NVARCHAR
(
11
)
, @DELETE_RULE NVARCHAR
(
11
)
,
@U_TABLE_SCHEMA sysname, @U_TABLE_NAME sysname,
@U_CONSTRAINT_NAME sysname, @U_CONSTRAINT_SCHEMA sysname;
DECLARE
C CURSOR
FOR
SELECT
TC.TABLE_SCHEMA, TC.TABLE_NAME
, RC.CONSTRAINT_SCHEMA
,
RC.CONSTRAINT_NAME
, MATCH_OPTION, UPDATE_RULE, DELETE_RULE,
TU.TABLE_SCHEMA AS
U_TABLE_SCHEMA, TU.TABLE_NAME
AS
U_TABLE_NAME,
TU.CONSTRAINT_SCHEMA
AS
U_CONSTRAINT_SCHEMA,
TU.CONSTRAINT_NAME
AS
U_CONSTRAINT_NAME
FROM
INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS
TC
INNER
JOIN
INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS
RC
ON
RC.CONSTRAINT_SCHEMA
=
TC.CONSTRAINT_SCHEMA
AND
RC.CONSTRAINT_NAME
=
TC.CONSTRAINT_NAME
INNER
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS
TU
ON
RC.UNIQUE_CONSTRAINT_SCHEMA =
TU.CONSTRAINT_SCHEMA
AND
RC.UNIQUE_CONSTRAINT_NAME =
TU.CONSTRAINT_NAME
FOR
READ
ONLY
;
OPEN
C;
FETCH
C INTO
@TABLE_SCHEMA, @TABLE_NAME
,
@CONSTRAINT_SCHEMA
, @CONSTRAINT_NAME
,
@MATCH_OPTION, @UPDATE_RULE, @DELETE_RULE,
@U_TABLE_SCHEMA, @U_TABLE_NAME,
@U_CONSTRAINT_SCHEMA, @U_CONSTRAINT_NAME;
WHILE
@@FETCH_STATUS =
0
BEGIN
SET
@COLS_FK =
''
;
SELECT
@COLS_FK =
@COLS_FK +
COLUMN_NAME
+
', '
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
TABLE_SCHEMA =
@TABLE_SCHEMA
AND
TABLE_NAME
=
@TABLE_NAME
AND
CONSTRAINT_SCHEMA
=
@CONSTRAINT_SCHEMA
AND
CONSTRAINT_NAME
=
@CONSTRAINT_NAME
;
SET
@COLS_FK =
SUBSTRING
(
@COLS_FK, 1
, LEN(
@COLS_FK)
-
1
)
;
SET
@COLS_UK =
''
;
SELECT
@COLS_UK =
@COLS_UK +
COLUMN_NAME
+
', '
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
TABLE_SCHEMA =
@U_TABLE_SCHEMA
AND
TABLE_NAME
=
@U_TABLE_NAME
AND
CONSTRAINT_SCHEMA
=
@U_CONSTRAINT_SCHEMA
AND
CONSTRAINT_NAME
=
@U_CONSTRAINT_NAME;
SET
@COLS_UK =
SUBSTRING
(
@COLS_UK, 1
, LEN(
@COLS_UK)
-
1
)
;
SET
@SQL
=
'ALTER TABLE ['
+
@TABLE_SCHEMA +
'].['
+
@TABLE_NAME
+
'] '
+
CASE
WHEN
@SQLCOMMAND =
'DROP'
THEN
'DROP CONSTRAINT ['
+
@CONSTRAINT_NAME
+
'];'
ELSE
'ADD CONSTRAINT ['
+
@CONSTRAINT_NAME
+
'] FOREIGN KEY ('
+
@COLS_FK
+
') REFERENCES ['
+
@U_TABLE_SCHEMA +
'].['
+
@U_TABLE_NAME
+
'] ('
+
@COLS_UK +
') MATCH '
+
@MATCH_OPTION
+
' ON UPDATE '
+
@UPDATE_RULE +
' ON DELETE '
+
@DELETE_RULE +
';'
END
;
PRINT
@SQL
;
FETCH
C INTO
@TABLE_SCHEMA, @TABLE_NAME
,
@CONSTRAINT_SCHEMA
, @CONSTRAINT_NAME
,
@MATCH_OPTION, @UPDATE_RULE, @DELETE_RULE,
@U_TABLE_SCHEMA, @U_TABLE_NAME,
@U_CONSTRAINT_SCHEMA, @U_CONSTRAINT_NAME;
END
CLOSE
C;
DEALLOCATE
C;
END
;
Cette procédure se basant sur les vues d'information de schéma de la norme SQL, vous n'aurez pas de difficulté à la remanier pour l'implanter au sein de votre SGBDR.
Exemple d'exécution :
EXEC
dbo.P_DDL_FK_CONSTRAINTS 'DROP'
V. C'est spécifique▲
Il n'y a rien de spécifique dans la gestion par contrainte de l'intégrité référentielle. Elle repose sur la norme SQL utilisée à ce niveau par tous les SGBDR. Tous les bons gestionnaires de bases de données relationnelles s'y sont mis, mais certains très tardivement il est vrai… Certes tout le monde ne respecte pas toutes les options disponibles de paramétrage des FOREIGN KEY mais ce n'est pas le plus important.
La syntaxe minimale qu'est CONSTRAINT … FOREIGN KEY (…) REFERENCES … (…) passe aujourd'hui sur tout les SGBDR transactionnels. Il n'y a donc aucune raison logique de donner un tel argument, sauf la méconnaissance, hélas bien répandue en matière de bases de données.
VI. En conclusion▲
Il n'y a rien de spécifique dans la gestion par contrainte de l'intégrité référentielle. Elle repose sur la norme SQL utilisée à ce niveau par tous les SGBDR. Tous les bons gestionnaires de bases de données relationnels s'y sont mis, mais certains très tardivement il est vrai… Certes tout le monde ne respecte pas toutes les options disponibles de paramétrage des FOREIGN KEY mais ce n'est pas le plus important.
VI-A. Arguments supplémentaires pour la mise en place des contraintes de clef étrangère▲
L'absence de contraintes d'intégrité référentielle a de multiples autres conséquences néfastes.
1) Cela empêche la pose des index les plus importants, ceux devant figurer sur les clefs étrangères. Or ces index sont en général tout à fait indispensables à un maintien basique des performances. En effet, la plupart des jointures des tables se font entre clef primaire et clef étrangère. En l'absence d'index sur ces éléments, les jointures seront lentes.
2) Cela interdit aux utilisateurs avancés comme aux administrateurs applicatifs de comprendre le modèle de données par la simple lecture des métadonnées de la base (vues SQL des contraintes FOREIGN KEY).
3) Cela empêche l'utilisation d'outil de rétro-ingénierie pour tenter de récupérer un modèle compréhensible, car ces outils se basent sur les métadonnées disponibles dans les vues système afin d'en déduire le modèle physique de données et, par là même, remonter à un MCD (modèle conceptuel de données).
4) Cela ne facilite pas les imports/exports du fait de la méconnaissance de l'imbrication des tables.
5) Cela lie encore plus l'utilisateur de la base à son créateur, chose que de nombreux éditeurs ont bien compris afin de rendre captifs leurs clients à un modèle opaque dans le but de vendre au prix fort des prestations qui n'auraient pas lieu d'être avec un modèle de données respectant les règles de l'art…
VI-B. Contreargument indéfendable : les progiciels▲
La plupart des mauvais éditeurs de progiciels (et ils sont hélas très nombreux) invoquent la nécessaire souplesse du modèle de données d'un progiciel pour défendre l'idée de l'absence de contrainte. C'est particulièrement stupide. En effet la pose de contrainte de clef étrangère est aussi dynamique que tout autre code SQL. Elle peut donc se faire à chaud, même si l'on rajoute une ou plusieurs tables au modèle.
Par exemple, lors de l'ajout dynamique d'une table à une base de données, dans le cas d'une nouvelle table liée à d'autres tables, la pose des différentes contraintes de clef étrangère permet d'automatiser la création de la jointure. En d'autres termes, grâce aux métadonnées des FOREIGN KEY, il est possible de réaliser des requêtes dont la jointure entre une nouvelle table créée dynamiquement et les autres tables de la base peut s'écrire automatiquement…
Démonstration : avec la requête SQL suivante :
WITH
T AS
(
SELECT
TCF.TABLE_SCHEMA +
'.'
+
TCF.TABLE_NAME
AS
TF,
TCU.TABLE_SCHEMA +
'.'
+
TCU.TABLE_NAME
AS
TM,
FK.COLUMN_NAME
AS
COL_FK, UK.COLUMN_NAME
AS
COL_UK,
ROW_NUMBER()
OVER(
ORDER
BY
FK.COLUMN_NAME
)
AS
N
FROM
INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS
RF
INNER
JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS
FK
ON
RF.CONSTRAINT_SCHEMA
=
FK.CONSTRAINT_SCHEMA
AND
RF.CONSTRAINT_NAME
=
FK.CONSTRAINT_NAME
INNER
JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS
UK
ON
RF.UNIQUE_CONSTRAINT_SCHEMA =
UK.CONSTRAINT_SCHEMA
AND
RF.UNIQUE_CONSTRAINT_NAME =
UK.CONSTRAINT_NAME
INNER
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS
TCF
ON
RF.CONSTRAINT_SCHEMA
=
TCF.CONSTRAINT_SCHEMA
AND
RF.CONSTRAINT_NAME
=
TCF.CONSTRAINT_NAME
INNER
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS
TCU
ON
RF.UNIQUE_CONSTRAINT_SCHEMA =
TCU.CONSTRAINT_SCHEMA
AND
RF.UNIQUE_CONSTRAINT_NAME =
TCU.CONSTRAINT_NAME
WHERE
TCF.TABLE_SCHEMA +
'.'
+
TCF.TABLE_NAME
=
@TF
AND
TCU.TABLE_SCHEMA +
'.'
+
TCU.TABLE_NAME
=
@TM)
,
TF AS
(
SELECT
TF, TM, COL_FK, COL_UK, N, MAX
(
N)
OVER()
AS
NN
FROM
T)
,
TR AS
(
SELECT
CAST
(
TF +
'.'
+
COL_FK +
' = '
+
TM +
'.'
+
COL_UK AS
VARCHAR
(
max
))
AS
JOINED, N, NN
FROM
TF
WHERE
N =
1
UNION
ALL
SELECT
CAST
(
JOINED +
' AND '
+
TF +
'.'
+
TF.COL_FK +
' = '
+
TM +
'.'
+
TF.COL_UK AS
VARCHAR
(
max
))
, TF.N, TF.NN
FROM
TF
INNER
JOIN
TR
ON
TF.N =
TR.N +
1
)
SELECT
JOINED
FROM
TR
WHERE
N =
NN;
Qui admet en paramètre @TF la table fille et @TM la table mère afin de fournir l'écriture de la jointure. À nouveau, cette requête utilise les vues normalisées d'information de schéma avec lesquelles tous les bons SGBDR travaillent. Au pire, si ce n'est pas le cas de votre SGBDR, rien n'empêche de créer ces vues en partant des tables système de votre SGBDR.
VI-C. La Loi, les règles de l'art▲
L'absence de FOREIGN KEY dans une base de données apporte de multiples préjudices que nous venons d'énumérer. Le fait que la structure de la base, comme le code applicatif, ne sont pas de votre œuvre, ne peut en aucun cas justifier l'absence de professionnalisme. Par conséquent, le préjudice subi par l'absence de ces FOREIGN KEY peut donc être condamné devant les tribunaux. En effet, les principes généraux du droit sont la Loi, mais aussi les usages (us et coutumes) et la jurisprudence.
En matière professionnelle, les us et coutumes sont appelés règles de l'art. En matière de bases de données, les règles de l'art sont basées sur les travaux des professionnels reconnus de tous, comme Frank Edgar Codd (père de la théorie des bases de données relationnelles), Peter Chen (père du modèle relationnel), Donald Chamberlin et Raymond Boyce (auteurs du langage SQL) et enfin Chris Date et Hugh Darwen, experts en bases de données relationnelles. Parmi les textes qui parlent des contraintes, citons les règles de Codd, le modèle entité/association (Peter Chen), la norme SQL (disponible à l'ISO) ou encore les ouvrages de Date et Darwen comme « An Introduction to Relational Database Theory », ou « SQL and Relational Theory » (How to Write Accurate SQL Code) ou encore « Logic and Databases »… Tous ces textes mentionnent les contraintes d'intégrité référentielle comme un élément essentiel et indispensable de la mécanique relationnelle.
Il ne viendrait pas à l'esprit à un maçon auquel on demande de construire un mur d'enceinte de le faire penché ou que son arase soit ondulée. Il est donc stupéfiant et condamnable de voir que certains informaticiens considèrent comme superflu de respecter les fondements mêmes de la création des bases de données relationnelles et s'abstiennent de toute contrainte d'intégrité, en particulier référentielle. On doit même se poser la question ?« À quoi sert un SGBDR si l'auteur de la structure de la base de données ne l'utilise pas de manière rationnelle ? » Ne serait-il pas mieux de revenir à la simple utilisation de fichiers Cobol de style ISAM ?
Lorsque l'on me demande mon avis pour choisir un logiciel dans une offre pléthorique, je conseille toujours de demander à l'éditeur le modèle conceptuel de données, car c'est celui qui sert de fondement à la modélisation des données. En effet, il faut partir du fait que, quels que soient la qualité du logiciel et son prix, ce n'est rien devant le capital que représentent vos données. N'oubliez jamais que vos données vous appartiennent. Choisir un outil incapable de les récupérer est souvent synonyme de gâchis.
Lorsqu'il n'y a pas de MCD ou que l'éditeur refuse de le communiquer (ce qui revient au même), je vous conseille vivement de vous pencher sur la structure de la base de données et si l'application vous tente vraiment de faire auditer la base.
Aucun refus d'aucune sorte n'est justifiable même lors de la phase d'avant vente. Certains éditeurs particulièrement peu scrupuleux vous glissent comme argument qu'étant donné que la base est leur œuvre, ils ne voudraient pas qu'elle soit pillée en la montrant à tout un chacun… C'est d'une grande bêtise, car lorsqu'on a une belle œuvre, on n'a pas honte de la montrer, d'autant que la justice protège du copiage la structure des bases de données. En effet, une jurisprudence constante démarrée il y a près de 100 ans (hé oui !) avec l'affaire Didot Bottin (CA Paris, 18 décembre 1924), renforcée par une directive européenne (du 11 mars 1996) traduite en droit français (loi n°98-536 du 1er juillet 1998 - Code de la Propriété Intellectuelle) protège la structure d'une base de données au titre des œuvres de l'esprit, exactement de la même manière que l'est tout code applicatif. Certains éditeurs honnêtes et parfaitement intelligents comme SAGE le savent bien et proposent à tous leurs clients de montrer comment a été dessinée l'architecture de la base et je dois dire que c'est souvent du bel ouvrage venant de chez eux… et pourtant il s'agit bien là aussi de progiciels !