Bonsoir RabDev,
Un détour du côté des contraintes d’inclusion.
Partons du MCD que vous proposez avec la version 0.5 de JMerise :
Et examinons la contrainte d’inclusion impliquant les associations « reservation » et « stocké dans ». Manifestement il manque une pointe de flèche, car en l’état, sans cet élément on ne sait pas quelle association est « incluse » dans l’autre.
On doit donc avoir la représentation :
Cette fois-ci, on sait comment interpréter l’inclusion sans ambiguïté.
Par ailleurs, le « manuel de l’utilisateur de JMerise » devrait fournir toutes les indications permettant de modéliser cette contrainte. Je rappelle à ce sujet la recommandation faite par l’afcet :
Exemple bref d’explication à usage du concepteur :
Dans l’exemple proposé par JMerise, l’association réservation est la portée de la contrainte et l’association Stocké dans en est la cible. Le pivot de la contrainte est composé des seules entités-types Dépôt et Article, tandis que l’entité-type Client ne joue aucun rôle dans la contrainte, bien qu’elle participe à l’association réservation : les lignes en pointillés sont là pour préciser que le pivot est composé des seules entités-types Dépôt et Article.
Passons au MLD.
Puisqu’elle disparaît lors du passage du MCD au MLD, j’ai fait figurer manuellement la contrainte d’inclusion (avec sa pointe de flèche...) :
Précisez qu’au stade MLD et SQL, l’expression de la contrainte est à la charge du concepteur...
Dans le style de la norme SQL :
CREATE ASSERTION RESERVER_INCLUSION_CHK
CHECK (
NOT EXISTS (SELECT articleId, depotId
FROM INSERTED
EXCEPT
SELECT articleId, depotId
FROM STOCKER
)
) ;
Les SGBD du marché renâclant à proposer l’instruction CREATE ASSERTION, on est obligé d’en passer par des triggers (lesquels en théorie sont là pour produire et non pas pour contrôler, mais bref, passons...)
Exemple avec SQL Server. Partons des déclarations suivantes :
CREATE TABLE DEPOT
(
depotId SERIAL NOT NULL
, depotKode VARCHAR(8) NOT NULL
, depotSigle VARCHAR(12) NOT NULL
, depotNom VARCHAR(64) NOT NULL
, depotAdresse VARCHAR(127) NOT NULL
, depotTel VARCHAR(24) NOT NULL
, CONSTRAINT DEPOT_PK PRIMARY KEY (depotId)
, CONSTRAINT DEPOT_CODE_AK UNIQUE (depotKode)
) ;
CREATE TABLE ARTICLE
(
articleId SERIAL NOT NULL
, articleCode VARCHAR(24) NOT NULL
, articleLibelle VARCHAR(128) NOT NULL
, articlePrixUnitaire DECIMAL(10,2) NOT NULL
, CONSTRAINT ARTICLE_PK PRIMARY KEY (articleId)
, CONSTRAINT ARTICLE_AK UNIQUE (articleCode)
) ;
CREATE TABLE CLIENT
(
clientId SERIAL NOT NULL
, clientCode VARCHAR(8) NOT NULL
, clientNom VARCHAR(64) NOT NULL
, clientAdresse VARCHAR(127) NOT NULL
, clientTel VARCHAR(24) NOT NULL
, CONSTRAINT CLIENT_PK PRIMARY KEY (clientId)
, CONSTRAINT CLIENT_CODE_AK UNIQUE (clientCode)
) ;
CREATE TABLE STOCKER
(
articleId INT NOT NULL
, depotId INT NOT NULL
, qteDisponible DECIMAL(8,2) NOT NULL
, CONSTRAINT STOCKER_PK PRIMARY KEY (articleId, depotId)
, CONSTRAINT STOCKER_DEPOT_FK FOREIGN KEY (depotId)
REFERENCES DEPOT
, CONSTRAINT STOCKER_ARTICLE_FK FOREIGN KEY (articleId)
REFERENCES ARTICLE
) ;
CREATE TABLE RESERVER
(
clientId INT NOT NULL
, articleId INT NOT NULL
, depotId INT NOT NULL
, qteReservee DECIMAL(8,2) NOT NULL
, CONSTRAINT RESERVER_PK PRIMARY KEY (clientId, articleId, depotId)
, CONSTRAINT RESERVER_CLIENT_FK FOREIGN KEY (clientId)
REFERENCES CLIENT
, CONSTRAINT RESERVER_ARTICLE_FK FOREIGN KEY (articleId)
REFERENCES ARTICLE
, CONSTRAINT RESERVER_DEPOT_FK FOREIGN KEY (depotId)
REFERENCES DEPOT
) ;
Contrôle par trigger de la contrainte d’inclusion avec SQL Server :
CREATE TRIGGER RESERVER_INCLUSION_TR ON RESERVER INSTEAD OF INSERT, UPDATE
AS
BEGIN
IF EXISTS
(SELECT articleId, depotId
FROM INSERTED
EXCEPT
SELECT articleId, depotId
FROM STOCKER
)
BEGIN
SELECT 'Viol de la contrainte d''inclusion !' AS Engueulade, * FROM INSERTED
----------------RAISERROR ('Viol de la contrainte d''inclusion ! !',16,1) -- state = 16 pour bloquer
RAISERROR ('Viol de la contrainte d''inclusion ! !',0,1) -- state = 0 pour les tests
RETURN
END
INSERT INTO RESERVER
SELECT clientId, articleId, depotId, qteReservee
FROM INSERTED
END
GO
Exemple avec PostgreSQL :
CREATE FUNCTION RESERVER_INCLUSION_FN()
RETURNS TRIGGER AS
$le_trigger$
DECLARE Erreur VARCHAR ;
BEGIN
IF EXISTS
(SELECT (articleId, depotId)
FROM RESERVER
EXCEPT
SELECT (articleId, depotId)
FROM STOCKER)
THEN
Erreur = 'Viol de la contrainte d''inclusion, la paire <articleId = '|| NEW.articleId || ', depotId = ' || NEW.depotId || '> est absente du stock.' ;
RAISE EXCEPTION SQLSTATE '45001' USING MESSAGE = Erreur ;
END IF ;
RETURN NEW ;
END ;
$le_trigger$
LANGUAGE plpgsql ;
CREATE TRIGGER RESERVER_INCLUSION_TR AFTER INSERT OR UPDATE ON RESERVER
FOR EACH ROW EXECUTE PROCEDURE RESERVER_INCLUSION_FN() ;
Avec MySQL (incomplet algébriquement parlant) :
DELIMITER GO
CREATE TRIGGER RESERVER_INCLUSION_TR_INSERT AFTER INSERT ON RESERVER
FOR EACH ROW
BEGIN
SET @Depot = (SELECT depotNom FROM DEPOT WHERE depotId = NEW.depotId) ;
SET @Article = (SELECT articleLibelle FROM ARTICLE WHERE articleId = NEW.articleId) ;
IF EXISTS (SELECT *
FROM RESERVER AS x LEFT JOIN STOCKER AS y ON x.articleId = y.articleId AND x.depotId = y.depotId
WHERE y.articleId IS NULL OR y.depotId IS NULL)
THEN
SET @erreur = CONCAT('Dépot = "', @Depot, '", Article = "', @Article, '" : demande de réservation incompatible avec le stock.') ;
SIGNAL SQLSTATE '45001' SET MESSAGE_TEXT = @erreur ;
END IF ;
END
GO
DELIMITER ;
MySQL ne permettant pas de déclarer le trigger à la fois pour INSERT et UPDATE, reste à créer le trigger pour UPDATE.
Suite à toutes ces horreurs qu’il faut développer, sachons qu’il existe un moyen simple et radical de mettre en oeuvre la contrainte d’inclusion, et d’envoyer tous ces triggers à la poubelle.
Reprenons la terminologie Afcet. Soit R l’association jouant le rôle de la portée (RESERVER dans l’exemple) et S l’association jouant le rôle de la cible (STOCKER dans l’exemple). Si lors du passage au MLD, R et S ont été transformées en tables, alors R est dotée des attributs (articleId, depotId dans l’exemple) permettant de définir une clé étrangère ({articleId, depotId} dans l’exemple) référençant la clé primaire (voire une clé alternative) de la table cible. Les clés étrangères jusque-là déclarées peuvent alors passer à la trappe.
Prenons la déclaration de la table RESERVER :
CREATE TABLE RESERVER
(
clientId INT NOT NULL
, articleId INT NOT NULL
, depotId INT NOT NULL
, qteReservee DECIMAL(8,2) NOT NULL
, CONSTRAINT RESERVER_PK PRIMARY KEY (clientId, articleId, depotId)
, CONSTRAINT RESERVER_CLIENT_FK FOREIGN KEY (clientId)
REFERENCES CLIENT
, CONSTRAINT RESERVER_ARTICLE_FK FOREIGN KEY (articleId)
REFERENCES ARTICLE
, CONSTRAINT RESERVER_DEPOT_FK FOREIGN KEY (depotId)
REFERENCES DEPOT)
;
Cette déclaration peut être remplacée par la suivante, indiscutablement bien plus robuste :
CREATE TABLE RESERVER
(
clientId INT NOT NULL
, articleId INT NOT NULL
, depotId INT NOT NULL
, qteReservee DECIMAL(8,2) NOT NULL
, CONSTRAINT RESERVER_PK PRIMARY KEY (clientId, articleId, depotId)
, CONSTRAINT RESERVER_CLIENT_FK FOREIGN KEY (clientId)
REFERENCES CLIENT
, CONSTRAINT RESERVER_STOCKER_FK FOREIGN KEY (articleId, depotId)
REFERENCES STOCKER
;
Et bien sûr, les triggers dégagent de facto à destination de la poubelle, ce qui est le but de la manœuvre...
MLD correspondant (autrement béton et sympathique) :
Remarques diverses
Dans votre exemple, il serait souhaitable d’éviter les identifiants significatifs, et d’en faire des identifiants alternatifs. Je rabâche, mais je ne me lasserai pas de citer Yves Tabourier...
Qui écrit à la page 80 de son remarquable ouvrage (De l’autre côté de MERISE, Les Éditions d’organisation, 1986), ce qui constitue une règle d’or valant pour les identifiants des entités-types florissant dans les MCD merisiens (règle d’or trop souvent méconnue, hélas ! Et comme disent Goethe et Cie, « ceux qui ont oublié le passé sont condamnés à le revivre... ») :
« ... la fonction d’une propriété est de décrire les objets (et les rencontres), alors que l’identifiant ne décrit rien. Son rôle fondamental est d’être sûr de distinguer deux jumeaux parfaits, malgré des descriptions identiques.
L’expérience montre d’ailleurs que l’usage des “identifiants significatifs” (ou “codes significatifs”) a pu provoquer des dégâts tellement coûteux que la sagesse est d’éviter avec le plus grand soin de construire des identifiants décrivant les objets ou, pis encore, leurs liens avec d’autres objets... »
Quand Tabourier parle des dégâts, je peux témoigner...
Et il ne faudrait pas que des débutants en modélisation se disent que les identifiants significatifs sont une bonne chose, pour avoir vu dans l’interface de JMerise l’attribut SIGLE comme identifiant de l’entité-type DEPOT...
MLD : il faudrait que l’on puisse présenter les attributs dans l’ordre qui nous convient (par exemple mettre avant les autres les attributs participant aux clés). Mais je suppose que vous êtes à la manœuvre à ce sujet.
MLD : évitez de cocher systématiquement NULL pour les clés des tables issues d’associations :
Code SQL. Type DECIMAL : n dont 2 devient parfois n dont -1...
CREATE TABLE public.ARTICLE(
articleId SERIAL NOT NULL ,
articleCode VARCHAR (24) NOT NULL ,
articleLibelle VARCHAR (127) NOT NULL ,
articlePrixUnitaire DECIMAL (10,-1) NOT NULL ,
...
CREATE TABLE public.STOCKER(
qteDisponible DECIMAL (8,-1) NOT NULL ,
articleId INT NOT NULL ,
depotId INT NOT NULL ,
...
CREATE TABLE public.RESERVER(
qteReservee DECIMAL (8,2) NOT NULL ,
articleId INT NOT NULL ,
depotId INT NOT NULL ,
clientId INT NOT NULL ,
Pour avoir quelque chose de correct, il faut actuellement éviter de saisir la partie décimale dans la fenêtre « Propriété de l’entité », mais le faire dans la fenêtre « Propriété de l’attribut »...
Encore bon courage !
Partager