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_nr
coincidente primero. -
Todavía puede agregar un
employee_nr
a cualquier usuario, y puede (entonces) seguir etiquetando cualquieruser_role
conis_employee
, independientemente delrole_name
real . Fácil de rechazar si es necesario, pero esta implementación no introduce más restricciones de las requeridas. -
users.is_employee
solo puede sertrue
ofalse
y se ve obligado a reflejar la existencia de unemployee_nr
por elCHECK
restricción. El disparador mantiene la columna sincronizada automáticamente. Podrías permitirfalse
adicionalmente para otros fines con solo actualizaciones menores al diseño. -
Las reglas para
user_role.is_employee
son ligeramente diferentes:debe ser cierto sirole_name = 'employee'
. Aplicado por unCHECK
restricción y se establece automáticamente por el disparador de nuevo. Pero está permitido cambiarrole_name
a otra cosa y seguir manteniendois_employee
. Nadie dijo un usuario con unemployee_nr
es 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 conCHECK
y restricciones FK, que no permiten excepciones. -
Aparte:pongo la columna
is_employee
primero en la restricciónUNIQUE (is_employee, users_id)
por una razón .users_id
ya está cubierto en PK, por lo que puede ocupar el segundo lugar aquí:
Entidades asociativas de base de datos e indexación