PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Contraintes de table croisée dans PostgreSQL

Clarifications

La formulation de cette exigence laisse place à l'interprétation :
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 un user.employee_nr correspondant d'abord.

  • Vous pouvez toujours ajouter un employee_nr à tout utilisateur, et vous pouvez (alors) toujours taguer n'importe quel user_role avec is_employee , quel que soit le role_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 que true ou false et est obligé de refléter l'existence d'un employee_nr par le CHECK contrainte. Le déclencheur maintient automatiquement la colonne synchronisée. Vous pouvez autoriser false 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 si role_name = 'employee' . Obligatoire par un CHECK contrainte et définie automatiquement par le déclencheur à nouveau. Mais il est permis de changer role_name à autre chose et toujours garder is_employee . Personne n'a dit un utilisateur avec un employee_nr est obligatoire pour avoir une entrée correspondante dans user_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 avec CHECK et les contraintes FK, qui n'autorisent aucune exception.

  • A part :j'ai mis la colonne is_employee premier dans la contrainte UNIQUE (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