Clarifications
La formulation de cette exigence laisse place à l'interprétation :
où UserRole.role_name
contient un nom de rôle d'employé.
Mon interprétation :
avec une entrée dans UserRole
qui a role_name = 'employee'
.
Votre convention de nommage est était problématique (mis à jour maintenant). User
est un mot réservé en SQL standard et Postgres. C'est illégal comme identifiant à moins d'être entre guillemets doubles - ce qui serait mal avisé. Les noms légaux des utilisateurs afin que vous n'ayez pas à mettre des guillemets doubles.
J'utilise des identifiants sans problème dans mon implémentation.
Le problème
FOREIGN KEY
et CHECK
contrainte sont les outils éprouvés et hermétiques pour faire respecter l'intégrité relationnelle. Les déclencheurs sont des fonctionnalités puissantes, utiles et polyvalentes, mais plus sophistiquées, moins strictes et avec plus de place pour les erreurs de conception et les cas particuliers.
Votre cas est délicat car une contrainte FK semble impossible au premier abord :elle nécessite une PRIMARY KEY
ou UNIQUE
contrainte de référence - aucun n'autorise les valeurs NULL. Il n'y a pas de contraintes FK partielles, les seules échappatoires à l'intégrité référentielle stricte sont les valeurs NULL dans le référencement colonnes en raison de la valeur par défaut MATCH SIMPLE
comportement des contraintes FK. Par documentation :
MATCH SIMPLE
permet à n'importe quelle colonne de clé étrangère d'être nulle ; si l'un d'entre eux est nul, la ligne n'est pas obligée d'avoir une correspondance dans la table référencée.
Réponse connexe sur dba.SE avec plus :
- Contrainte de clé étrangère à deux colonnes uniquement lorsque la troisième colonne est NOT NULL
La solution consiste à introduire un indicateur booléen is_employee
pour marquer les employés des deux côtés, défini NOT NULL
dans users
, mais autorisé à être NULL
dans user_role
:
Solution
Cela applique vos exigences exactement , tout en minimisant le bruit et les frais généraux :
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
C'est tout.
Ces déclencheurs sont facultatifs mais recommandés pour plus de commodité pour définir les balises ajoutées is_employee
automatiquement et vous n'avez rien à faire supplémentaire :
-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
Encore une fois, simple, optimisé et appelé uniquement en cas de besoin.
Violon SQL démo pour Postgres 9.3. Devrait fonctionner avec Postgres 9.1+.
Points majeurs
-
Maintenant, si nous voulons définir
user_role.role_name = 'employee'
, alors il doit y avoir unuser.employee_nr
correspondant d'abord. -
Vous pouvez toujours ajouter un
employee_nr
à tout utilisateur, et vous pouvez (alors) toujours taguer n'importe queluser_role
avecis_employee
, quel que soit lerole_name
réel . Facile à interdire si nécessaire, mais cette implémentation n'introduit pas plus de restrictions que nécessaire. -
users.is_employee
ne peut être quetrue
oufalse
et est obligé de refléter l'existence d'unemployee_nr
par leCHECK
contrainte. Le déclencheur maintient automatiquement la colonne synchronisée. Vous pouvez autoriserfalse
en plus à d'autres fins avec seulement des mises à jour mineures de la conception. -
Les règles pour
user_role.is_employee
sont légèrement différents :il doit être vrai sirole_name = 'employee'
. Obligatoire par unCHECK
contrainte et définie automatiquement par le déclencheur à nouveau. Mais il est permis de changerrole_name
à autre chose et toujours garderis_employee
. Personne n'a dit un utilisateur avec unemployee_nr
est obligatoire pour avoir une entrée correspondante dansuser_role
, juste l'inverse ! Encore une fois, facile à appliquer en plus si nécessaire. -
S'il y a d'autres déclencheurs qui pourraient interférer, considérez ceci :
Comment éviter les appels de déclencheur en boucle dans PostgreSQL 9.2.1
Mais nous n'avons pas à nous inquiéter que les règles puissent être violées car les déclencheurs ci-dessus ne sont que pour des raisons de commodité. Les règles en soi sont appliquées avecCHECK
et les contraintes FK, qui n'autorisent aucune exception. -
A part :j'ai mis la colonne
is_employee
premier dans la contrainteUNIQUE (is_employee, users_id)
pour une raison .users_id
est déjà couvert dans la PK, il peut donc prendre la deuxième place ici :
Entités associatives et indexation de la BD