Aclaraciones
La formulación de este requisito deja espacio para la interpretación:
donde UserRole.role_name contiene un nombre de función de empleado.
Mi interpretación:
con una entrada en UserRole que tiene role_name = 'employee' .
Tu convención de nomenclatura es fue problemático (actualizado ahora). User es una palabra reservada en SQL estándar y Postgres. Es ilegal como identificador a menos que esté entre comillas dobles, lo que sería desaconsejable. Nombres legales de usuario para que no tenga que usar comillas dobles.
Estoy usando identificadores sin problemas en mi implementación.
El problema
FOREIGN KEY y CHECK Las restricciones son las herramientas probadas y herméticas para hacer cumplir la integridad relacional. Los activadores son funciones potentes, útiles y versátiles, pero más sofisticadas, menos estrictas y con más espacio para errores de diseño y casos de esquina.
Su caso es difícil porque una restricción FK parece imposible al principio:requiere una PRIMARY KEY o UNIQUE restricción a la referencia - tampoco permite valores NULL. No hay restricciones FK parciales, el único escape de la integridad referencial estricta son los valores NULL en la referencia columnas debido al MATCH SIMPLE predeterminado comportamiento de las restricciones FK. Por documentación:
MATCH SIMPLE permite que cualquiera de las columnas de clave externa sea nula; si alguno de ellos es nulo, no se requiere que la fila tenga una coincidencia en la tabla a la que se hace referencia.
Respuesta relacionada en dba.SE con más:
- Restricción de clave externa de dos columnas solo cuando la tercera columna NO es NULL
La solución es introducir un indicador booleano is_employee para marcar empleados en ambos lados, definido NOT NULL en users , pero se permite que sea NULL en user_role :
Solución
Esto hace cumplir sus requisitos exactamente , manteniendo al mínimo el ruido y la sobrecarga:
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)
);
Eso es todo.
Estos desencadenantes son opcionales pero se recomiendan por conveniencia para establecer las etiquetas agregadas is_employee automáticamente y no tienes que hacer nada adicional:
-- 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();
Nuevamente, sin tonterías, optimizado y solo llamado cuando sea necesario.
Violín SQL demostración para Postgres 9.3. Debería funcionar con Postgres 9.1+.
Puntos principales
-
Ahora, si queremos establecer
user_role.role_name = 'employee', entonces tiene que haber unuser.employee_nrcoincidente primero. -
Todavía puede agregar un
employee_nra cualquier usuario, y puede (entonces) seguir etiquetando cualquieruser_roleconis_employee, independientemente delrole_namereal . Fácil de rechazar si es necesario, pero esta implementación no introduce más restricciones de las requeridas. -
users.is_employeesolo puede sertrueofalsey se ve obligado a reflejar la existencia de unemployee_nrpor elCHECKrestricción. El disparador mantiene la columna sincronizada automáticamente. Podrías permitirfalseadicionalmente para otros fines con solo actualizaciones menores al diseño. -
Las reglas para
user_role.is_employeeson ligeramente diferentes:debe ser cierto sirole_name = 'employee'. Aplicado por unCHECKrestricción y se establece automáticamente por el disparador de nuevo. Pero está permitido cambiarrole_namea otra cosa y seguir manteniendois_employee. Nadie dijo un usuario con unemployee_nres obligatorio tener una entrada correspondiente enuser_role, ¡al revés! Nuevamente, fácil de aplicar adicionalmente si es necesario. -
Si hay otros disparadores que podrían interferir, considere esto:
Cómo evitar llamadas de disparador en bucle en PostgreSQL 9.2.1
Pero no debemos preocuparnos de que se puedan violar las reglas porque los disparadores anteriores son solo por conveniencia. Las reglas en sí se aplican conCHECKy restricciones FK, que no permiten excepciones. -
Aparte:pongo la columna
is_employeeprimero en la restricciónUNIQUE (is_employee, users_id)por una razón .users_idya está cubierto en PK, por lo que puede ocupar el segundo lugar aquí:
Entidades asociativas de base de datos e indexación