Chiarimenti
La formulazione di questo requisito lascia spazio all'interpretazione:
dove UserRole.role_name
contiene un nome di ruolo dipendente.
La mia interpretazione:
con una voce in UserRole
che ha role_name = 'employee'
.
La tua convenzione di denominazione è era problematico (aggiornato ora). User
è una parola riservata in SQL standard e Postgres. È illegale come identificatore a meno che non sia doppiato, il che sarebbe sconsiderato. Nomi legali dell'utente in modo da non dover virgolette.
Sto utilizzando identificatori senza problemi nella mia implementazione.
Il problema
FOREIGN KEY
e CHECK
vincolo sono gli strumenti collaudati e ermetici per rafforzare l'integrità relazionale. I trigger sono funzionalità potenti, utili e versatili ma più sofisticate, meno rigide e con più spazio per errori di progettazione e casi angolari.
Il tuo caso è difficile perché un vincolo FK sembra impossibile all'inizio:richiede una PRIMARY KEY
o UNIQUE
vincolo di riferimento - nessuno dei due consente valori NULL. Non ci sono vincoli FK parziali, l'unica via di fuga dalla rigorosa integrità referenziale sono i valori NULL nel riferimento colonne a causa del valore predefinito MATCH SIMPLE
comportamento dei vincoli FK. Per documentazione:
MATCH SIMPLE
consente a qualsiasi colonna di chiave esterna di essere nulla; se qualcuno di essi è nullo, non è necessario che la riga abbia una corrispondenza nella tabella di riferimento.
Risposta correlata su dba.SE con altro:
- vincolo di chiave esterna a due colonne solo quando la terza colonna NON è NULL
La soluzione alternativa è introdurre un flag booleano is_employee
per contrassegnare i dipendenti su entrambi i lati, definito NOT NULL
in users
, ma può essere NULL
in user_role
:
Soluzione
Questo fa rispettare i tuoi requisiti esattamente , riducendo al minimo rumore e spese generali:
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)
);
Questo è tutto.
Questi trigger sono facoltativi ma consigliati per comodità di impostare i tag aggiunti is_employee
automaticamente e non devi fare nulla extra:
-- 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();
Ancora una volta, senza fronzoli, ottimizzato e chiamato solo quando necessario.
SQL Fiddle demo per Postgres 9.3. Dovrebbe funzionare con Postgres 9.1+.
Punti principali
-
Ora, se vogliamo impostare
user_role.role_name = 'employee'
, quindi deve esserci unuser.employee_nr
corrispondente prima. -
Puoi comunque aggiungere un
employee_nr
a qualsiasi utente e puoi (quindi) ancora taggare qualsiasiuser_role
conis_employee
, indipendentemente dall'effettivorole_name
. Facile da disabilitare se necessario, ma questa implementazione non introduce più restrizioni di quelle richieste. -
users.is_employee
può essere solotrue
ofalse
ed è costretto a riflettere l'esistenza di unemployee_nr
dalCHECK
vincolo. Il trigger mantiene la colonna sincronizzata automaticamente. Potresti consentirefalse
inoltre per altri scopi con solo piccoli aggiornamenti al design. -
Le regole per
user_role.is_employee
sono leggermente diversi:deve essere vero serole_name = 'employee'
. Applicato da unCHECK
vincolo e impostato di nuovo automaticamente dal trigger. Ma è consentito cambiarerole_name
a qualcos'altro e mantieni ancorais_employee
. Nessuno ha detto un utente con unemployee_nr
è richiesto per avere una voce corrispondente inuser_role
, proprio il contrario! Ancora una volta, facile da applicare ulteriormente se necessario. -
Se ci sono altri trigger che potrebbero interferire, considera questo:
Come evitare il looping delle chiamate trigger in PostgreSQL 9.2.1
Ma non dobbiamo preoccuparci che le regole possano essere violate perché i trigger di cui sopra sono solo per comodità. Le regole di per sé vengono applicate conCHECK
e vincoli FK, che non consentono eccezioni. -
A parte:metto la colonna
is_employee
prima nel vincoloUNIQUE (is_employee, users_id)
per un motivo .users_id
è già trattato nel PK, quindi può occupare il secondo posto qui:
Entità associative DB e indicizzazione