sql >> Base de Datos >  >> RDS >> PostgreSQL

Restricciones de tablas cruzadas en PostgreSQL

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 un user.employee_nr coincidente primero.

  • Todavía puede agregar un employee_nr a cualquier usuario, y puede (entonces) seguir etiquetando cualquier user_role con is_employee , independientemente del role_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 ser true o false y se ve obligado a reflejar la existencia de un employee_nr por el CHECK restricción. El disparador mantiene la columna sincronizada automáticamente. Podrías permitir false adicionalmente para otros fines con solo actualizaciones menores al diseño.

  • Las reglas para user_role.is_employee son ligeramente diferentes:debe ser cierto si role_name = 'employee' . Aplicado por un CHECK restricción y se establece automáticamente por el disparador de nuevo. Pero está permitido cambiar role_name a otra cosa y seguir manteniendo is_employee . Nadie dijo un usuario con un employee_nr es obligatorio tener una entrada correspondiente en user_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 con CHECK y restricciones FK, que no permiten excepciones.

  • Aparte:pongo la columna is_employee primero en la restricción UNIQUE (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